up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Policy.Engine.Ledger;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Snapshots;
|
||||
|
||||
internal sealed record SnapshotSummary(
|
||||
[property: JsonPropertyName("snapshot_id")] string SnapshotId,
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("ledger_export_id")] string LedgerExportId,
|
||||
[property: JsonPropertyName("generated_at")] string GeneratedAt,
|
||||
[property: JsonPropertyName("status_counts")] IReadOnlyDictionary<string, int> StatusCounts);
|
||||
|
||||
internal sealed record SnapshotDetail(
|
||||
[property: JsonPropertyName("snapshot_id")] string SnapshotId,
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("ledger_export_id")] string LedgerExportId,
|
||||
[property: JsonPropertyName("generated_at")] string GeneratedAt,
|
||||
[property: JsonPropertyName("overlay_hash")] string OverlayHash,
|
||||
[property: JsonPropertyName("status_counts")] IReadOnlyDictionary<string, int> StatusCounts,
|
||||
[property: JsonPropertyName("records")] IReadOnlyList<LedgerExportRecord> Records);
|
||||
|
||||
internal sealed record SnapshotRequest(
|
||||
[property: JsonPropertyName("tenant_id")] string TenantId,
|
||||
[property: JsonPropertyName("overlay_hash")] string OverlayHash);
|
||||
@@ -0,0 +1,76 @@
|
||||
using StellaOps.Policy.Engine.Ledger;
|
||||
using StellaOps.Policy.Engine.Orchestration;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Snapshots;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot API stub (POLICY-ENGINE-35-201) built on ledger exports.
|
||||
/// </summary>
|
||||
internal sealed class SnapshotService
|
||||
{
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly LedgerExportService _ledger;
|
||||
private readonly ISnapshotStore _store;
|
||||
|
||||
public SnapshotService(
|
||||
TimeProvider timeProvider,
|
||||
LedgerExportService ledger,
|
||||
ISnapshotStore store)
|
||||
{
|
||||
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
_ledger = ledger ?? throw new ArgumentNullException(nameof(ledger));
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
}
|
||||
|
||||
public async Task<SnapshotDetail> CreateAsync(
|
||||
SnapshotRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var exports = await _ledger.ListAsync(request.TenantId, cancellationToken).ConfigureAwait(false);
|
||||
var export = exports.LastOrDefault();
|
||||
if (export is null)
|
||||
{
|
||||
export = await _ledger.BuildAsync(new LedgerExportRequest(request.TenantId), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var statusCounts = export.Records
|
||||
.GroupBy(r => r.Status)
|
||||
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.Ordinal);
|
||||
|
||||
var generatedAt = _timeProvider.GetUtcNow().ToString("O");
|
||||
var snapshotId = StableIdGenerator.CreateUlid($"{export.Manifest.ExportId}|{request.OverlayHash}");
|
||||
|
||||
var snapshot = new SnapshotDetail(
|
||||
SnapshotId: snapshotId,
|
||||
TenantId: request.TenantId,
|
||||
LedgerExportId: export.Manifest.ExportId,
|
||||
GeneratedAt: generatedAt,
|
||||
OverlayHash: request.OverlayHash,
|
||||
StatusCounts: statusCounts,
|
||||
Records: export.Records);
|
||||
|
||||
await _store.SaveAsync(snapshot, cancellationToken).ConfigureAwait(false);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _store.GetAsync(snapshotId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<SnapshotSummary> Items, string? NextCursor)> ListAsync(
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshots = await _store.ListAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
var summaries = snapshots
|
||||
.OrderByDescending(s => s.GeneratedAt, StringComparer.Ordinal)
|
||||
.Select(s => new SnapshotSummary(s.SnapshotId, s.TenantId, s.LedgerExportId, s.GeneratedAt, s.StatusCounts))
|
||||
.ToList();
|
||||
|
||||
return (summaries, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Snapshots;
|
||||
|
||||
internal interface ISnapshotStore
|
||||
{
|
||||
Task SaveAsync(SnapshotDetail snapshot, CancellationToken cancellationToken = default);
|
||||
Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<SnapshotDetail>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class InMemorySnapshotStore : ISnapshotStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SnapshotDetail> _snapshots = new(StringComparer.Ordinal);
|
||||
|
||||
public Task SaveAsync(SnapshotDetail snapshot, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
_snapshots[snapshot.SnapshotId] = snapshot;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<SnapshotDetail?> GetAsync(string snapshotId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_snapshots.TryGetValue(snapshotId, out var value);
|
||||
return Task.FromResult(value);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SnapshotDetail>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IEnumerable<SnapshotDetail> items = _snapshots.Values;
|
||||
if (!string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
items = items.Where(s => string.Equals(s.TenantId, tenantId, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
var ordered = items
|
||||
.OrderBy(s => s.GeneratedAt, StringComparer.Ordinal)
|
||||
.ThenBy(s => s.SnapshotId, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<SnapshotDetail>>(ordered);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user