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:
@@ -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(
|
||||
|
||||
37
src/Aoc/__Libraries/StellaOps.Aoc/AocError.cs
Normal file
37
src/Aoc/__Libraries/StellaOps.Aoc/AocError.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user