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

- 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:
master
2025-11-07 10:01:35 +02:00
parent e5ffcd6535
commit a1ce3f74fa
122 changed files with 8730 additions and 914 deletions

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Core.Contracts;
@@ -37,7 +38,12 @@ internal sealed record SurfaceManifestRequest(
string? Version,
string? WorkerInstance);
internal sealed class SurfaceManifestPublisher
internal interface ISurfaceManifestPublisher
{
Task<SurfaceManifestPublishResult> PublishAsync(SurfaceManifestRequest request, CancellationToken cancellationToken);
}
internal sealed class SurfaceManifestPublisher : ISurfaceManifestPublisher
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{

View File

@@ -1,14 +1,20 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.EntryTrace.Serialization;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Worker.Diagnostics;
namespace StellaOps.Scanner.Worker.Processing.Surface;
@@ -20,15 +26,30 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly SurfaceManifestPublisher _publisher;
private static readonly JsonSerializerOptions ManifestSerializerOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private readonly ISurfaceManifestPublisher _publisher;
private readonly ISurfaceCache _surfaceCache;
private readonly ISurfaceEnvironment _surfaceEnvironment;
private readonly ScannerWorkerMetrics _metrics;
private readonly ILogger<SurfaceManifestStageExecutor> _logger;
private readonly string _componentVersion;
public SurfaceManifestStageExecutor(
SurfaceManifestPublisher publisher,
ISurfaceManifestPublisher publisher,
ISurfaceCache surfaceCache,
ISurfaceEnvironment surfaceEnvironment,
ScannerWorkerMetrics metrics,
ILogger<SurfaceManifestStageExecutor> logger)
{
_publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
_surfaceCache = surfaceCache ?? throw new ArgumentNullException(nameof(surfaceCache));
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
_metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_componentVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
}
@@ -42,23 +63,49 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
var payloads = CollectPayloads(context);
if (payloads.Count == 0)
{
_metrics.RecordSurfaceManifestSkipped(context);
_logger.LogDebug("No surface payloads available for job {JobId}; skipping manifest publish.", context.JobId);
return;
}
var request = new SurfaceManifestRequest(
ScanId: context.ScanId,
ImageDigest: ResolveImageDigest(context),
Attempt: context.Lease.Attempt,
Metadata: context.Lease.Metadata,
Payloads: payloads,
Component: "scanner.worker",
Version: _componentVersion,
WorkerInstance: Environment.MachineName);
var tenant = _surfaceEnvironment.Settings?.Tenant ?? string.Empty;
var stopwatch = Stopwatch.StartNew();
var result = await _publisher.PublishAsync(request, cancellationToken).ConfigureAwait(false);
context.Analysis.Set(ScanAnalysisKeys.SurfaceManifest, result);
_logger.LogInformation("Surface manifest stored for job {JobId} with digest {Digest}.", context.JobId, result.ManifestDigest);
try
{
await PersistPayloadsToSurfaceCacheAsync(context, tenant, payloads, cancellationToken).ConfigureAwait(false);
var request = new SurfaceManifestRequest(
ScanId: context.ScanId,
ImageDigest: ResolveImageDigest(context),
Attempt: context.Lease.Attempt,
Metadata: context.Lease.Metadata,
Payloads: payloads,
Component: "scanner.worker",
Version: _componentVersion,
WorkerInstance: Environment.MachineName);
var result = await _publisher.PublishAsync(request, cancellationToken).ConfigureAwait(false);
await PersistManifestToSurfaceCacheAsync(context, tenant, result, cancellationToken).ConfigureAwait(false);
context.Analysis.Set(ScanAnalysisKeys.SurfaceManifest, result);
stopwatch.Stop();
_metrics.RecordSurfaceManifestPublished(context, payloads.Count, stopwatch.Elapsed);
_logger.LogInformation("Surface manifest stored for job {JobId} with digest {Digest}.", context.JobId, result.ManifestDigest);
}
catch (OperationCanceledException)
{
stopwatch.Stop();
throw;
}
catch (Exception ex)
{
stopwatch.Stop();
_metrics.RecordSurfaceManifestFailed(context, ex.GetType().Name);
_logger.LogError(ex, "Failed to persist surface manifest for job {JobId}.", context.JobId);
throw;
}
}
private List<SurfaceManifestPayload> CollectPayloads(ScanJobContext context)
@@ -118,6 +165,56 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return payloads;
}
private async Task PersistPayloadsToSurfaceCacheAsync(
ScanJobContext context,
string tenant,
IReadOnlyList<SurfaceManifestPayload> payloads,
CancellationToken cancellationToken)
{
if (payloads.Count == 0)
{
return;
}
foreach (var payload in payloads)
{
cancellationToken.ThrowIfCancellationRequested();
var digest = ComputeDigest(payload.Content.Span);
var normalizedKind = NormalizeKind(payload.Kind);
var cacheKey = CreateArtifactCacheKey(tenant, normalizedKind, digest);
await _surfaceCache.SetAsync(cacheKey, payload.Content, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Cached surface payload {Kind} for job {JobId} with digest {Digest}.",
normalizedKind,
context.JobId,
digest);
_metrics.RecordSurfacePayloadPersisted(context, normalizedKind);
}
}
private async Task PersistManifestToSurfaceCacheAsync(
ScanJobContext context,
string tenant,
SurfaceManifestPublishResult result,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var manifestBytes = JsonSerializer.SerializeToUtf8Bytes(result.Document, ManifestSerializerOptions);
var cacheKey = CreateManifestCacheKey(tenant, result.ManifestDigest);
await _surfaceCache.SetAsync(cacheKey, manifestBytes, cancellationToken).ConfigureAwait(false);
_logger.LogDebug(
"Cached surface manifest for job {JobId} with digest {Digest}.",
context.JobId,
result.ManifestDigest);
}
private static string ResolveImageDigest(ScanJobContext context)
{
static bool TryGet(IReadOnlyDictionary<string, string> metadata, string key, out string value)
@@ -143,5 +240,46 @@ internal sealed class SurfaceManifestStageExecutor : IScanStageExecutor
return context.ScanId;
}
private static SurfaceCacheKey CreateArtifactCacheKey(string tenant, string kind, string digest)
{
var @namespace = $"surface.artifacts.{kind}";
var contentKey = NormalizeDigestForKey(digest);
return new SurfaceCacheKey(@namespace, tenant, contentKey);
}
private static SurfaceCacheKey CreateManifestCacheKey(string tenant, string digest)
{
var contentKey = NormalizeDigestForKey(digest);
return new SurfaceCacheKey("surface.manifests", tenant, contentKey);
}
private static string NormalizeKind(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "unknown";
}
var trimmed = value.Trim();
return trimmed.ToLowerInvariant();
}
private static string NormalizeDigestForKey(string? digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return string.Empty;
}
return digest.Trim();
}
private static string ComputeDigest(ReadOnlySpan<byte> content)
{
Span<byte> hash = stackalloc byte[32];
SHA256.HashData(content, hash);
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
}
private static readonly IFormatProvider CultureInfoInvariant = System.Globalization.CultureInfo.InvariantCulture;
}