Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// InMemoryGateEvaluationQueue.cs
|
||||
// Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
||||
// Task: CICD-GATE-02 - Gate evaluation queue implementation
|
||||
// Description: In-memory queue for gate evaluation jobs with background processing
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Threading.Channels;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Gateway.Endpoints;
|
||||
|
||||
namespace StellaOps.Policy.Gateway.Services;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of the gate evaluation queue.
|
||||
/// Uses System.Threading.Channels for async producer-consumer pattern.
|
||||
/// </summary>
|
||||
public sealed class InMemoryGateEvaluationQueue : IGateEvaluationQueue
|
||||
{
|
||||
private readonly Channel<GateEvaluationJob> _channel;
|
||||
private readonly ILogger<InMemoryGateEvaluationQueue> _logger;
|
||||
|
||||
public InMemoryGateEvaluationQueue(ILogger<InMemoryGateEvaluationQueue> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
_logger = logger;
|
||||
|
||||
// Bounded channel to prevent unbounded memory growth
|
||||
_channel = Channel.CreateBounded<GateEvaluationJob>(new BoundedChannelOptions(1000)
|
||||
{
|
||||
FullMode = BoundedChannelFullMode.Wait,
|
||||
SingleReader = false,
|
||||
SingleWriter = false
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> EnqueueAsync(GateEvaluationRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var jobId = GenerateJobId();
|
||||
var job = new GateEvaluationJob
|
||||
{
|
||||
JobId = jobId,
|
||||
Request = request,
|
||||
QueuedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _channel.Writer.WriteAsync(job, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Enqueued gate evaluation job {JobId} for {Repository}@{Digest}",
|
||||
jobId,
|
||||
request.Repository,
|
||||
request.ImageDigest);
|
||||
|
||||
return jobId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel reader for consuming jobs.
|
||||
/// </summary>
|
||||
public ChannelReader<GateEvaluationJob> Reader => _channel.Reader;
|
||||
|
||||
private static string GenerateJobId()
|
||||
{
|
||||
// Format: gate-{timestamp}-{random}
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var random = Guid.NewGuid().ToString("N")[..8];
|
||||
return $"gate-{timestamp}-{random}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A gate evaluation job in the queue.
|
||||
/// </summary>
|
||||
public sealed record GateEvaluationJob
|
||||
{
|
||||
public required string JobId { get; init; }
|
||||
public required GateEvaluationRequest Request { get; init; }
|
||||
public required DateTimeOffset QueuedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Background service that processes gate evaluation jobs from the queue.
|
||||
/// Orchestrates: image analysis → drift delta computation → gate evaluation.
|
||||
/// </summary>
|
||||
public sealed class GateEvaluationWorker : BackgroundService
|
||||
{
|
||||
private readonly InMemoryGateEvaluationQueue _queue;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<GateEvaluationWorker> _logger;
|
||||
|
||||
public GateEvaluationWorker(
|
||||
InMemoryGateEvaluationQueue queue,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<GateEvaluationWorker> logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queue);
|
||||
ArgumentNullException.ThrowIfNull(scopeFactory);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_queue = queue;
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Gate evaluation worker starting");
|
||||
|
||||
await foreach (var job in _queue.Reader.ReadAllAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessJobAsync(job, stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogError(ex,
|
||||
"Error processing gate evaluation job {JobId} for {Repository}@{Digest}",
|
||||
job.JobId,
|
||||
job.Request.Repository,
|
||||
job.Request.ImageDigest);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Gate evaluation worker stopping");
|
||||
}
|
||||
|
||||
private async Task ProcessJobAsync(GateEvaluationJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Processing gate evaluation job {JobId} for {Repository}@{Digest}",
|
||||
job.JobId,
|
||||
job.Request.Repository,
|
||||
job.Request.ImageDigest);
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var evaluator = scope.ServiceProvider.GetRequiredService<IDriftGateEvaluator>();
|
||||
|
||||
// Build a minimal context for the gate evaluation.
|
||||
// In production, this would involve:
|
||||
// 1. Fetching or triggering a scan of the image
|
||||
// 2. Computing the reachability delta against the baseline
|
||||
// 3. Building the DriftGateContext with actual metrics
|
||||
//
|
||||
// For now, we create a placeholder context that represents "no drift detected"
|
||||
// which allows the gate to pass. The full implementation requires Scanner integration.
|
||||
var driftContext = new DriftGateContext
|
||||
{
|
||||
DeltaReachable = 0,
|
||||
DeltaUnreachable = 0,
|
||||
HasKevReachable = false,
|
||||
BaseScanId = job.Request.BaselineRef,
|
||||
HeadScanId = job.Request.ImageDigest
|
||||
};
|
||||
|
||||
var evalRequest = new DriftGateRequest
|
||||
{
|
||||
Context = driftContext,
|
||||
PolicyId = null, // Use default policy
|
||||
AllowOverride = false
|
||||
};
|
||||
|
||||
var result = await evaluator.EvaluateAsync(evalRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Gate evaluation {JobId} completed: Decision={Decision}, GateCount={GateCount}",
|
||||
job.JobId,
|
||||
result.Decision,
|
||||
result.Gates.Length);
|
||||
|
||||
// TODO: Store result and notify via webhook/event
|
||||
// This will be implemented in CICD-GATE-03
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user