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

@@ -44,11 +44,9 @@ public static class AocHttpResults
throw new ArgumentNullException(nameof(exception));
}
var primaryCode = exception.Result.Violations.IsDefaultOrEmpty
? "ERR_AOC_000"
: exception.Result.Violations[0].ErrorCode;
var error = AocError.FromException(exception, detail);
var violationPayload = exception.Result.Violations
var violationPayload = error.Violations
.Select(v => new Dictionary<string, object?>(StringComparer.Ordinal)
{
["code"] = v.ErrorCode,
@@ -59,8 +57,9 @@ public static class AocHttpResults
var extensionPayload = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["code"] = primaryCode,
["violations"] = violationPayload
["code"] = error.Code,
["violations"] = violationPayload,
["error"] = error
};
if (extensions is not null)
@@ -71,9 +70,9 @@ public static class AocHttpResults
}
}
var statusCode = status ?? MapErrorCodeToStatus(primaryCode);
var statusCode = status ?? MapErrorCodeToStatus(error.Code);
var problemType = type ?? DefaultProblemType;
var problemDetail = detail ?? $"AOC guard rejected the request with {primaryCode}.";
var problemDetail = detail ?? error.Message;
var problemTitle = title ?? "Aggregation-Only Contract violation";
return HttpResults.Problem(

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace StellaOps.Aoc;
/// <summary>
/// Represents a structured Aggregation-Only Contract error payload.
/// </summary>
public sealed record AocError(
[property: JsonPropertyName("code")] string Code,
[property: JsonPropertyName("message")] string Message,
[property: JsonPropertyName("violations")] ImmutableArray<AocViolation> Violations)
{
public static AocError FromResult(AocGuardResult result, string? message = null)
{
if (result is null)
{
throw new ArgumentNullException(nameof(result));
}
var violations = result.Violations;
var code = violations.IsDefaultOrEmpty ? "ERR_AOC_000" : violations[0].ErrorCode;
var resolvedMessage = message ?? $"AOC guard rejected the payload with {code}.";
return new(code, resolvedMessage, violations);
}
public static AocError FromException(AocGuardException exception, string? message = null)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
return FromResult(exception.Result, message);
}
}

View File

@@ -1,29 +1,49 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Aoc;
public sealed record AocGuardOptions
{
private static readonly ImmutableHashSet<string> DefaultRequiredTopLevel = new[]
{
"tenant",
"source",
"upstream",
"content",
"linkset",
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
public static AocGuardOptions Default { get; } = new();
public ImmutableHashSet<string> RequiredTopLevelFields { get; init; } = DefaultRequiredTopLevel;
/// <summary>
/// When true, signature metadata is required under upstream.signature.
/// </summary>
public bool RequireSignatureMetadata { get; init; } = true;
/// <summary>
/// When true, tenant must be a non-empty string.
/// </summary>
public bool RequireTenant { get; init; } = true;
}
public sealed record AocGuardOptions
{
private static readonly ImmutableHashSet<string> DefaultRequiredTopLevel = new[]
{
"tenant",
"source",
"upstream",
"content",
"linkset",
}.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
private static readonly ImmutableHashSet<string> DefaultAllowedTopLevel = DefaultRequiredTopLevel
.Union(new[]
{
"_id",
"identifiers",
"attributes",
"supersedes",
"createdAt",
"created_at",
"ingestedAt",
"ingested_at"
}, StringComparer.OrdinalIgnoreCase)
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
public static AocGuardOptions Default { get; } = new();
public ImmutableHashSet<string> RequiredTopLevelFields { get; init; } = DefaultRequiredTopLevel;
/// <summary>
/// Optional allowlist for top-level fields. Unknown fields trigger ERR_AOC_007.
/// </summary>
public ImmutableHashSet<string> AllowedTopLevelFields { get; init; } = DefaultAllowedTopLevel;
/// <summary>
/// When true, signature metadata is required under upstream.signature.
/// </summary>
public bool RequireSignatureMetadata { get; init; } = true;
/// <summary>
/// When true, tenant must be a non-empty string.
/// </summary>
public bool RequireTenant { get; init; } = true;
}

View File

@@ -11,16 +11,17 @@ public interface IAocGuard
public sealed class AocWriteGuard : IAocGuard
{
public AocGuardResult Validate(JsonElement document, AocGuardOptions? options = null)
{
options ??= AocGuardOptions.Default;
var violations = ImmutableArray.CreateBuilder<AocViolation>();
var presentTopLevel = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var property in document.EnumerateObject())
{
presentTopLevel.Add(property.Name);
public AocGuardResult Validate(JsonElement document, AocGuardOptions? options = null)
{
options ??= AocGuardOptions.Default;
var violations = ImmutableArray.CreateBuilder<AocViolation>();
var presentTopLevel = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var allowedTopLevelFields = options.AllowedTopLevelFields ?? AocGuardOptions.Default.AllowedTopLevelFields;
foreach (var property in document.EnumerateObject())
{
presentTopLevel.Add(property.Name);
if (AocForbiddenKeys.IsForbiddenTopLevel(property.Name))
{
violations.Add(AocViolation.Create(AocViolationCode.ForbiddenField, $"/{property.Name}", $"Field '{property.Name}' is forbidden in AOC documents."));
@@ -28,14 +29,20 @@ public sealed class AocWriteGuard : IAocGuard
}
if (AocForbiddenKeys.IsDerivedField(property.Name))
{
violations.Add(AocViolation.Create(AocViolationCode.DerivedFindingDetected, $"/{property.Name}", $"Derived field '{property.Name}' must not be written during ingestion."));
}
}
foreach (var required in options.RequiredTopLevelFields)
{
if (!document.TryGetProperty(required, out var element) || element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
violations.Add(AocViolation.Create(AocViolationCode.DerivedFindingDetected, $"/{property.Name}", $"Derived field '{property.Name}' must not be written during ingestion."));
}
if (!allowedTopLevelFields.Contains(property.Name))
{
violations.Add(AocViolation.Create(AocViolationCode.UnknownField, $"/{property.Name}", $"Field '{property.Name}' is not allowed in AOC documents."));
continue;
}
}
foreach (var required in options.RequiredTopLevelFields)
{
if (!document.TryGetProperty(required, out var element) || element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
{
violations.Add(AocViolation.Create(AocViolationCode.MissingRequiredField, $"/{required}", $"Required field '{required}' is missing."));
continue;