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:
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using StellaOps.Scanner.Surface.FS;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorCommandSurfaceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task DescriptorCommand_PublishesSurfaceArtifacts()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var casRoot = Path.Combine(temp.Path, "cas");
|
||||
Directory.CreateDirectory(casRoot);
|
||||
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.6\"}");
|
||||
|
||||
var layerFragmentsPath = Path.Combine(temp.Path, "layer-fragments.json");
|
||||
await File.WriteAllTextAsync(layerFragmentsPath, "[]");
|
||||
|
||||
var entryTraceGraphPath = Path.Combine(temp.Path, "entrytrace-graph.json");
|
||||
await File.WriteAllTextAsync(entryTraceGraphPath, "{\"nodes\":[],\"edges\":[]}");
|
||||
|
||||
var entryTraceNdjsonPath = Path.Combine(temp.Path, "entrytrace.ndjson");
|
||||
await File.WriteAllTextAsync(entryTraceNdjsonPath, "{}\n{}");
|
||||
|
||||
var manifestOutputPath = Path.Combine(temp.Path, "out", "surface-manifest.json");
|
||||
|
||||
var repoRoot = TestPathHelper.FindRepositoryRoot();
|
||||
var manifestDirectory = Path.Combine(repoRoot, "src", "Scanner", "StellaOps.Scanner.Sbomer.BuildXPlugin");
|
||||
var pluginAssembly = typeof(BuildxPluginManifest).Assembly.Location;
|
||||
|
||||
var psi = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = repoRoot
|
||||
};
|
||||
|
||||
psi.ArgumentList.Add(pluginAssembly);
|
||||
psi.ArgumentList.Add("descriptor");
|
||||
psi.ArgumentList.Add("--manifest");
|
||||
psi.ArgumentList.Add(manifestDirectory);
|
||||
psi.ArgumentList.Add("--cas");
|
||||
psi.ArgumentList.Add(casRoot);
|
||||
psi.ArgumentList.Add("--image");
|
||||
psi.ArgumentList.Add("sha256:feedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedfacefeedface");
|
||||
psi.ArgumentList.Add("--sbom");
|
||||
psi.ArgumentList.Add(sbomPath);
|
||||
psi.ArgumentList.Add("--sbom-name");
|
||||
psi.ArgumentList.Add("sample.cdx.json");
|
||||
psi.ArgumentList.Add("--surface-layer-fragments");
|
||||
psi.ArgumentList.Add(layerFragmentsPath);
|
||||
psi.ArgumentList.Add("--surface-entrytrace-graph");
|
||||
psi.ArgumentList.Add(entryTraceGraphPath);
|
||||
psi.ArgumentList.Add("--surface-entrytrace-ndjson");
|
||||
psi.ArgumentList.Add(entryTraceNdjsonPath);
|
||||
psi.ArgumentList.Add("--surface-cache-root");
|
||||
psi.ArgumentList.Add(casRoot);
|
||||
psi.ArgumentList.Add("--surface-tenant");
|
||||
psi.ArgumentList.Add("test-tenant");
|
||||
psi.ArgumentList.Add("--surface-manifest-output");
|
||||
psi.ArgumentList.Add(manifestOutputPath);
|
||||
|
||||
var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start BuildX plug-in process.");
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync();
|
||||
var stderr = await process.StandardError.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
Assert.True(process.ExitCode == 0, $"Descriptor command failed.\nSTDOUT: {stdout}\nSTDERR: {stderr}");
|
||||
|
||||
var descriptor = JsonSerializer.Deserialize<DescriptorDocument>(stdout, new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
Assert.NotNull(descriptor);
|
||||
Assert.Equal("stellaops.buildx.descriptor.v1", descriptor!.Schema);
|
||||
Assert.Equal("sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c", descriptor.Artifact.Digest);
|
||||
|
||||
Assert.Contains("surface manifest stored", stderr, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.True(File.Exists(manifestOutputPath));
|
||||
|
||||
var surfaceManifestPath = Directory.GetFiles(Path.Combine(casRoot, "scanner", "surface", "manifests"), "*.json", SearchOption.AllDirectories).Single();
|
||||
var manifestDocument = JsonSerializer.Deserialize<SurfaceManifestDocument>(await File.ReadAllTextAsync(surfaceManifestPath), new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||
Assert.NotNull(manifestDocument);
|
||||
Assert.Equal("test-tenant", manifestDocument!.Tenant);
|
||||
Assert.Equal(3, manifestDocument.Artifacts.Count);
|
||||
|
||||
foreach (var artifact in manifestDocument.Artifacts)
|
||||
{
|
||||
Assert.StartsWith("cas://", artifact.Uri, StringComparison.Ordinal);
|
||||
var localPath = ResolveLocalPath(artifact.Uri, casRoot);
|
||||
Assert.True(File.Exists(localPath), $"Missing CAS object for {artifact.Uri}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveLocalPath(string casUri, string casRoot)
|
||||
{
|
||||
const string prefix = "cas://";
|
||||
if (!casUri.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported CAS URI {casUri}.");
|
||||
}
|
||||
|
||||
var slashIndex = casUri.IndexOf(/, prefix.Length);
|
||||
if (slashIndex < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"CAS URI {casUri} does not contain a bucket path.");
|
||||
}
|
||||
|
||||
var relative = casUri[(slashIndex + 1)..];
|
||||
var localPath = Path.Combine(casRoot, relative.Replace(/, Path.DirectorySeparatorChar));
|
||||
return localPath;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
{
|
||||
"schema": "stellaops.buildx.descriptor.v1",
|
||||
"generatedAt": "2025-10-18T12:00:00\u002B00:00",
|
||||
"generator": {
|
||||
"name": "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"digest": "sha256:0123456789abcdef"
|
||||
},
|
||||
"artifact": {
|
||||
"mediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"size": 45,
|
||||
"annotations": {
|
||||
"org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson",
|
||||
"org.stellaops.scanner.version": "1.2.3",
|
||||
"org.stellaops.sbom.kind": "inventory",
|
||||
"org.stellaops.sbom.format": "cyclonedx-json",
|
||||
"org.stellaops.provenance.status": "pending",
|
||||
"org.stellaops.provenance.dsse.sha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
|
||||
"org.stellaops.provenance.nonce": "a608acf859cd58a8389816b8d9eb2a07",
|
||||
"org.stellaops.license.id": "lic-123",
|
||||
"org.opencontainers.image.title": "sample.cdx.json",
|
||||
"org.stellaops.repository": "git.stella-ops.org/stellaops"
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"status": "pending",
|
||||
"expectedDsseSha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
|
||||
"nonce": "a608acf859cd58a8389816b8d9eb2a07",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance",
|
||||
"predicateType": "https://slsa.dev/provenance/v1"
|
||||
},
|
||||
"metadata": {
|
||||
"sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"sbomPath": "sample.cdx.json",
|
||||
"sbomMediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"repository": "git.stella-ops.org/stellaops",
|
||||
"buildRef": "refs/heads/main",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance"
|
||||
}
|
||||
{
|
||||
"schema": "stellaops.buildx.descriptor.v1",
|
||||
"generatedAt": "2025-10-18T12:00:00\u002B00:00",
|
||||
"generator": {
|
||||
"name": "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"digest": "sha256:0123456789abcdef"
|
||||
},
|
||||
"artifact": {
|
||||
"mediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"size": 45,
|
||||
"annotations": {
|
||||
"org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson",
|
||||
"org.stellaops.scanner.version": "1.2.3",
|
||||
"org.stellaops.sbom.kind": "inventory",
|
||||
"org.stellaops.sbom.format": "cyclonedx-json",
|
||||
"org.stellaops.provenance.status": "pending",
|
||||
"org.stellaops.provenance.dsse.sha256": "sha256:35ab4784f3bad40bb0063b522939ac729cf43d2012059947c0e56475d682c05e",
|
||||
"org.stellaops.provenance.nonce": "5e13230e3dcbc8be996d8132d92e8826",
|
||||
"org.stellaops.license.id": "lic-123",
|
||||
"org.opencontainers.image.title": "sample.cdx.json",
|
||||
"org.stellaops.repository": "git.stella-ops.org/stellaops"
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"status": "pending",
|
||||
"expectedDsseSha256": "sha256:35ab4784f3bad40bb0063b522939ac729cf43d2012059947c0e56475d682c05e",
|
||||
"nonce": "5e13230e3dcbc8be996d8132d92e8826",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance",
|
||||
"predicateType": "https://slsa.dev/provenance/v1"
|
||||
},
|
||||
"metadata": {
|
||||
"sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"sbomPath": "sample.cdx.json",
|
||||
"sbomMediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"repository": "git.stella-ops.org/stellaops",
|
||||
"buildRef": "refs/heads/main",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Surface;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Surface;
|
||||
|
||||
public sealed class SurfaceManifestWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task WriteAsync_PersistsArtifactsAndManifest()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var fragmentsPath = Path.Combine(temp.Path, "layer-fragments.json");
|
||||
await File.WriteAllTextAsync(fragmentsPath, "[]");
|
||||
|
||||
var graphPath = Path.Combine(temp.Path, "entrytrace-graph.json");
|
||||
await File.WriteAllTextAsync(graphPath, "{\"nodes\":[],\"edges\":[]}");
|
||||
|
||||
var ndjsonPath = Path.Combine(temp.Path, "entrytrace.ndjson");
|
||||
await File.WriteAllTextAsync(ndjsonPath, "{}\n{}");
|
||||
|
||||
var manifestOutputPath = Path.Combine(temp.Path, "out", "surface-manifest.json");
|
||||
|
||||
var options = new SurfaceOptions(
|
||||
CacheRoot: temp.Path,
|
||||
CacheBucket: "scanner-artifacts",
|
||||
RootPrefix: "scanner",
|
||||
Tenant: "tenant-a",
|
||||
Component: "scanner.buildx",
|
||||
ComponentVersion: "1.2.3",
|
||||
WorkerInstance: "builder-01",
|
||||
Attempt: 2,
|
||||
ImageDigest: "sha256:feedface",
|
||||
ScanId: "scan-123",
|
||||
LayerFragmentsPath: fragmentsPath,
|
||||
EntryTraceGraphPath: graphPath,
|
||||
EntryTraceNdjsonPath: ndjsonPath,
|
||||
ManifestOutputPath: manifestOutputPath);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System);
|
||||
var result = await writer.WriteAsync(options, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result!.Document.Source);
|
||||
Assert.Equal("tenant-a", result.Document.Tenant);
|
||||
Assert.Equal("scanner.buildx", result.Document.Source!.Component);
|
||||
Assert.Equal("1.2.3", result.Document.Source.Version);
|
||||
Assert.Equal(3, result.Document.Artifacts.Count);
|
||||
|
||||
var kinds = result.Document.Artifacts.Select(a => a.Kind).ToHashSet();
|
||||
Assert.Contains("entrytrace.graph", kinds);
|
||||
Assert.Contains("entrytrace.ndjson", kinds);
|
||||
Assert.Contains("layer.fragments", kinds);
|
||||
|
||||
Assert.True(File.Exists(result.ManifestPath));
|
||||
Assert.True(File.Exists(manifestOutputPath));
|
||||
|
||||
foreach (var artifact in result.Artifacts)
|
||||
{
|
||||
Assert.True(File.Exists(artifact.FilePath));
|
||||
Assert.False(string.IsNullOrWhiteSpace(artifact.ManifestArtifact.Uri));
|
||||
Assert.StartsWith("cas://scanner-artifacts/", artifact.ManifestArtifact.Uri, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_NoArtifacts_ReturnsNull()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var options = new SurfaceOptions(
|
||||
CacheRoot: temp.Path,
|
||||
CacheBucket: "scanner-artifacts",
|
||||
RootPrefix: "scanner",
|
||||
Tenant: "tenant-a",
|
||||
Component: "scanner.buildx",
|
||||
ComponentVersion: "1.0",
|
||||
WorkerInstance: "builder-01",
|
||||
Attempt: 1,
|
||||
ImageDigest: "sha256:deadbeef",
|
||||
ScanId: "scan-1",
|
||||
LayerFragmentsPath: null,
|
||||
EntryTraceGraphPath: null,
|
||||
EntryTraceNdjsonPath: null,
|
||||
ManifestOutputPath: null);
|
||||
|
||||
var writer = new SurfaceManifestWriter(TimeProvider.System);
|
||||
var result = await writer.WriteAsync(options, CancellationToken.None);
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
|
||||
internal static class TestPathHelper
|
||||
{
|
||||
public static string FindRepositoryRoot()
|
||||
{
|
||||
var current = AppContext.BaseDirectory;
|
||||
for (var i = 0; i < 15 && !string.IsNullOrWhiteSpace(current); i++)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current, "global.json")))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = Directory.GetParent(current)?.FullName;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to locate repository root (global.json not found).");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user