Implement MongoDB-based storage for Pack Run approval, artifact, log, and state management
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added MongoPackRunApprovalStore for managing approval states with MongoDB. - Introduced MongoPackRunArtifactUploader for uploading and storing artifacts. - Created MongoPackRunLogStore to handle logging of pack run events. - Developed MongoPackRunStateStore for persisting and retrieving pack run states. - Implemented unit tests for MongoDB stores to ensure correct functionality. - Added MongoTaskRunnerTestContext for setting up MongoDB test environment. - Enhanced PackRunStateFactory to correctly initialize state with gate reasons.
This commit is contained in:
@@ -30,7 +30,10 @@ public sealed class ScannerSurfaceSecretConfiguratorTests
|
||||
""";
|
||||
|
||||
using var handle = SurfaceSecretHandle.FromBytes(Encoding.UTF8.GetBytes(json));
|
||||
var secretProvider = new StubSecretProvider(handle);
|
||||
var secretProvider = new StubSecretProvider(new Dictionary<string, SurfaceSecretHandle>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["cas-access"] = handle
|
||||
});
|
||||
var environment = new StubSurfaceEnvironment();
|
||||
var options = new ScannerWebServiceOptions();
|
||||
|
||||
@@ -82,17 +85,101 @@ public sealed class ScannerSurfaceSecretConfiguratorTests
|
||||
Assert.Equal("X-Sync", storageOptions.ObjectStore.RustFs.ApiKeyHeader);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_AppliesAttestationSecretToSigning()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"keyPem": "-----BEGIN KEY-----\nYWJj\n-----END KEY-----",
|
||||
"certificatePem": "CERT-PEM",
|
||||
"certificateChainPem": "CHAIN-PEM"
|
||||
}
|
||||
""";
|
||||
|
||||
using var handle = SurfaceSecretHandle.FromBytes(Encoding.UTF8.GetBytes(json));
|
||||
var secretProvider = new StubSecretProvider(new Dictionary<string, SurfaceSecretHandle>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["attestation"] = handle
|
||||
});
|
||||
var environment = new StubSurfaceEnvironment();
|
||||
var options = new ScannerWebServiceOptions();
|
||||
|
||||
var configurator = new ScannerSurfaceSecretConfigurator(
|
||||
secretProvider,
|
||||
environment,
|
||||
NullLogger<ScannerSurfaceSecretConfigurator>.Instance);
|
||||
|
||||
configurator.Configure(options);
|
||||
|
||||
Assert.Equal("-----BEGIN KEY-----\nYWJj\n-----END KEY-----", options.Signing.KeyPem);
|
||||
Assert.Equal("CERT-PEM", options.Signing.CertificatePem);
|
||||
Assert.Equal("CHAIN-PEM", options.Signing.CertificateChainPem);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_AppliesRegistrySecretToOptions()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"defaultRegistry": "registry.example.com",
|
||||
"entries": [
|
||||
{
|
||||
"registry": "registry.example.com",
|
||||
"username": "demo",
|
||||
"password": "secret",
|
||||
"scopes": ["repo:sample:pull"],
|
||||
"headers": { "X-Test": "value" },
|
||||
"allowInsecureTls": true,
|
||||
"email": "demo@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
using var handle = SurfaceSecretHandle.FromBytes(Encoding.UTF8.GetBytes(json));
|
||||
var secretProvider = new StubSecretProvider(new Dictionary<string, SurfaceSecretHandle>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["registry"] = handle
|
||||
});
|
||||
var environment = new StubSurfaceEnvironment();
|
||||
var options = new ScannerWebServiceOptions();
|
||||
|
||||
var configurator = new ScannerSurfaceSecretConfigurator(
|
||||
secretProvider,
|
||||
environment,
|
||||
NullLogger<ScannerSurfaceSecretConfigurator>.Instance);
|
||||
|
||||
configurator.Configure(options);
|
||||
|
||||
Assert.Equal("registry.example.com", options.Registry.DefaultRegistry);
|
||||
var credential = Assert.Single(options.Registry.Credentials);
|
||||
Assert.Equal("registry.example.com", credential.Registry);
|
||||
Assert.Equal("demo", credential.Username);
|
||||
Assert.Equal("secret", credential.Password);
|
||||
Assert.True(credential.AllowInsecureTls);
|
||||
Assert.Contains("repo:sample:pull", credential.Scopes);
|
||||
Assert.Equal("value", credential.Headers["X-Test"]);
|
||||
Assert.Equal("demo@example.com", credential.Email);
|
||||
}
|
||||
|
||||
private sealed class StubSecretProvider : ISurfaceSecretProvider
|
||||
{
|
||||
private readonly SurfaceSecretHandle _handle;
|
||||
private readonly IDictionary<string, SurfaceSecretHandle> _handles;
|
||||
|
||||
public StubSecretProvider(SurfaceSecretHandle handle)
|
||||
public StubSecretProvider(IDictionary<string, SurfaceSecretHandle> handles)
|
||||
{
|
||||
_handle = handle;
|
||||
_handles = handles ?? throw new ArgumentNullException(nameof(handles));
|
||||
}
|
||||
|
||||
public ValueTask<SurfaceSecretHandle> GetAsync(SurfaceSecretRequest request, CancellationToken cancellationToken = default)
|
||||
=> ValueTask.FromResult(_handle);
|
||||
{
|
||||
if (_handles.TryGetValue(request.SecretType, out var handle))
|
||||
{
|
||||
return ValueTask.FromResult(handle);
|
||||
}
|
||||
|
||||
throw new SurfaceSecretNotFoundException(request);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubSurfaceEnvironment : ISurfaceEnvironment
|
||||
|
||||
@@ -9,7 +9,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -17,6 +17,7 @@ using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.EntryTrace.Serialization;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
@@ -92,39 +93,88 @@ public sealed class ScansEndpointsTests
|
||||
|
||||
using var factory = new ScannerApplicationFactory();
|
||||
|
||||
const string manifestDigest = "sha256:b2efc2d1f8b042b7f168bcb7d4e2f8e91d36b8306bd855382c5f847efc2c1111";
|
||||
const string graphDigest = "sha256:9a0d4f8c7b6a5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a291819";
|
||||
const string ndjsonDigest = "sha256:3f2e1d0c9b8a7f6e5d4c3b2a1908f7e6d5c4b3a29181726354433221100ffeec";
|
||||
const string fragmentsDigest = "sha256:aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55aa55";
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var artifactRepository = scope.ServiceProvider.GetRequiredService<ArtifactRepository>();
|
||||
var linkRepository = scope.ServiceProvider.GetRequiredService<LinkRepository>();
|
||||
var artifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.ImageBom, digest);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var artifact = new ArtifactDocument
|
||||
async Task InsertAsync(
|
||||
ArtifactDocumentType type,
|
||||
ArtifactDocumentFormat format,
|
||||
string artifactDigest,
|
||||
string mediaType,
|
||||
string ttlClass)
|
||||
{
|
||||
Id = artifactId,
|
||||
Type = ArtifactDocumentType.ImageBom,
|
||||
Format = ArtifactDocumentFormat.CycloneDxJson,
|
||||
MediaType = "application/vnd.cyclonedx+json; version=1.6; view=inventory",
|
||||
BytesSha256 = digest,
|
||||
SizeBytes = 2048,
|
||||
Immutable = true,
|
||||
RefCount = 1,
|
||||
TtlClass = "default",
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
var artifactId = CatalogIdFactory.CreateArtifactId(type, artifactDigest);
|
||||
var document = new ArtifactDocument
|
||||
{
|
||||
Id = artifactId,
|
||||
Type = type,
|
||||
Format = format,
|
||||
MediaType = mediaType,
|
||||
BytesSha256 = artifactDigest,
|
||||
SizeBytes = 2048,
|
||||
Immutable = true,
|
||||
RefCount = 1,
|
||||
TtlClass = ttlClass,
|
||||
CreatedAtUtc = now,
|
||||
UpdatedAtUtc = now
|
||||
};
|
||||
|
||||
await artifactRepository.UpsertAsync(artifact, CancellationToken.None).ConfigureAwait(false);
|
||||
await artifactRepository.UpsertAsync(document, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var link = new LinkDocument
|
||||
{
|
||||
Id = CatalogIdFactory.CreateLinkId(LinkSourceType.Image, digest, artifactId),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = digest,
|
||||
ArtifactId = artifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
var link = new LinkDocument
|
||||
{
|
||||
Id = CatalogIdFactory.CreateLinkId(LinkSourceType.Image, digest, artifactId),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = digest,
|
||||
ArtifactId = artifactId,
|
||||
CreatedAtUtc = now
|
||||
};
|
||||
|
||||
await linkRepository.UpsertAsync(link, CancellationToken.None).ConfigureAwait(false);
|
||||
await linkRepository.UpsertAsync(link, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await InsertAsync(
|
||||
ArtifactDocumentType.ImageBom,
|
||||
ArtifactDocumentFormat.CycloneDxJson,
|
||||
digest,
|
||||
"application/vnd.cyclonedx+json; version=1.6; view=inventory",
|
||||
"default").ConfigureAwait(false);
|
||||
|
||||
await InsertAsync(
|
||||
ArtifactDocumentType.SurfaceManifest,
|
||||
ArtifactDocumentFormat.SurfaceManifestJson,
|
||||
manifestDigest,
|
||||
"application/vnd.stellaops.surface.manifest+json",
|
||||
"surface.manifest").ConfigureAwait(false);
|
||||
|
||||
await InsertAsync(
|
||||
ArtifactDocumentType.SurfaceEntryTrace,
|
||||
ArtifactDocumentFormat.EntryTraceGraphJson,
|
||||
graphDigest,
|
||||
"application/json",
|
||||
"surface.payload").ConfigureAwait(false);
|
||||
|
||||
await InsertAsync(
|
||||
ArtifactDocumentType.SurfaceEntryTrace,
|
||||
ArtifactDocumentFormat.EntryTraceNdjson,
|
||||
ndjsonDigest,
|
||||
"application/x-ndjson",
|
||||
"surface.payload").ConfigureAwait(false);
|
||||
|
||||
await InsertAsync(
|
||||
ArtifactDocumentType.SurfaceLayerFragment,
|
||||
ArtifactDocumentFormat.ComponentFragmentJson,
|
||||
fragmentsDigest,
|
||||
"application/json",
|
||||
"surface.payload").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
using var client = factory.CreateClient();
|
||||
@@ -160,15 +210,46 @@ public sealed class ScansEndpointsTests
|
||||
Assert.Equal(digest, manifest.ImageDigest);
|
||||
Assert.Equal(surface.Tenant, manifest.Tenant);
|
||||
Assert.NotEqual(default, manifest.GeneratedAt);
|
||||
var manifestArtifact = Assert.Single(manifest.Artifacts);
|
||||
Assert.Equal("sbom-inventory", manifestArtifact.Kind);
|
||||
Assert.Equal("cdx-json", manifestArtifact.Format);
|
||||
Assert.Equal(digest, manifestArtifact.Digest);
|
||||
Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=inventory", manifestArtifact.MediaType);
|
||||
Assert.Equal("inventory", manifestArtifact.View);
|
||||
var artifactsByKind = manifest.Artifacts.ToDictionary(a => a.Kind, StringComparer.Ordinal);
|
||||
Assert.Equal(5, artifactsByKind.Count);
|
||||
|
||||
var expectedUri = $"cas://scanner-artifacts/scanner/images/{digestValue}/sbom.cdx.json";
|
||||
Assert.Equal(expectedUri, manifestArtifact.Uri);
|
||||
static string BuildUri(ArtifactDocumentType type, ArtifactDocumentFormat format, string digestValue)
|
||||
=> $"cas://scanner-artifacts/{ArtifactObjectKeyBuilder.Build(type, format, digestValue, \"scanner\")}";
|
||||
|
||||
var inventory = artifactsByKind["sbom-inventory"];
|
||||
Assert.Equal(digest, inventory.Digest);
|
||||
Assert.Equal("cdx-json", inventory.Format);
|
||||
Assert.Equal("application/vnd.cyclonedx+json; version=1.6; view=inventory", inventory.MediaType);
|
||||
Assert.Equal("inventory", inventory.View);
|
||||
Assert.Equal(BuildUri(ArtifactDocumentType.ImageBom, ArtifactDocumentFormat.CycloneDxJson, digest), inventory.Uri);
|
||||
|
||||
var manifestArtifact = artifactsByKind["surface.manifest"];
|
||||
Assert.Equal(manifestDigest, manifestArtifact.Digest);
|
||||
Assert.Equal("surface.manifest", manifestArtifact.Format);
|
||||
Assert.Equal("application/vnd.stellaops.surface.manifest+json", manifestArtifact.MediaType);
|
||||
Assert.Null(manifestArtifact.View);
|
||||
Assert.Equal(BuildUri(ArtifactDocumentType.SurfaceManifest, ArtifactDocumentFormat.SurfaceManifestJson, manifestDigest), manifestArtifact.Uri);
|
||||
|
||||
var graphArtifact = artifactsByKind["entrytrace.graph"];
|
||||
Assert.Equal(graphDigest, graphArtifact.Digest);
|
||||
Assert.Equal("entrytrace.graph", graphArtifact.Format);
|
||||
Assert.Equal("application/json", graphArtifact.MediaType);
|
||||
Assert.Null(graphArtifact.View);
|
||||
Assert.Equal(BuildUri(ArtifactDocumentType.SurfaceEntryTrace, ArtifactDocumentFormat.EntryTraceGraphJson, graphDigest), graphArtifact.Uri);
|
||||
|
||||
var ndjsonArtifact = artifactsByKind["entrytrace.ndjson"];
|
||||
Assert.Equal(ndjsonDigest, ndjsonArtifact.Digest);
|
||||
Assert.Equal("entrytrace.ndjson", ndjsonArtifact.Format);
|
||||
Assert.Equal("application/x-ndjson", ndjsonArtifact.MediaType);
|
||||
Assert.Null(ndjsonArtifact.View);
|
||||
Assert.Equal(BuildUri(ArtifactDocumentType.SurfaceEntryTrace, ArtifactDocumentFormat.EntryTraceNdjson, ndjsonDigest), ndjsonArtifact.Uri);
|
||||
|
||||
var fragmentsArtifact = artifactsByKind["layer.fragments"];
|
||||
Assert.Equal(fragmentsDigest, fragmentsArtifact.Digest);
|
||||
Assert.Equal("layer.fragments", fragmentsArtifact.Format);
|
||||
Assert.Equal("application/json", fragmentsArtifact.MediaType);
|
||||
Assert.Equal("inventory", fragmentsArtifact.View);
|
||||
Assert.Equal(BuildUri(ArtifactDocumentType.SurfaceLayerFragment, ArtifactDocumentFormat.ComponentFragmentJson, fragmentsDigest), fragmentsArtifact.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user