up
Some checks failed
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -27,4 +27,4 @@ Host signed Task Pack bundles with provenance and RBAC for Epic 12. Ensure pac
|
||||
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
|
||||
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
|
||||
- 6. Registry API expectations: require `X-API-Key` when configured and tenant scoping via `X-StellaOps-Tenant` (or `tenantId` on upload). Content/provenance downloads must emit digest headers (`X-Content-Digest`, `X-Provenance-Digest`) and respect tenant allowlists.
|
||||
- 7. Lifecycle/parity/signature rotation endpoints require tenant headers; offline seed export supports per-tenant filtering and deterministic zip output. All mutating calls emit audit log entries (file `audit.ndjson` or Mongo `packs_audit_log`).
|
||||
- 7. Lifecycle/parity/signature rotation endpoints require tenant headers; offline seed export supports per-tenant filtering and deterministic zip output. All mutating calls emit audit log entries (file `audit.ndjson` or file-backed audit logs).
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoAttestationRepository : IAttestationRepository
|
||||
{
|
||||
private readonly IMongoCollection<AttestationDocument> _index;
|
||||
private readonly IMongoCollection<AttestationBlob> _blobs;
|
||||
|
||||
public MongoAttestationRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
_index = database.GetCollection<AttestationDocument>(options.AttestationCollection ?? "packs_attestations");
|
||||
_blobs = database.GetCollection<AttestationBlob>(options.AttestationBlobsCollection ?? "packs_attestation_blobs");
|
||||
_index.Indexes.CreateOne(new CreateIndexModel<AttestationDocument>(Builders<AttestationDocument>.IndexKeys.Ascending(x => x.PackId).Ascending(x => x.Type), new CreateIndexOptions { Unique = true }));
|
||||
_blobs.Indexes.CreateOne(new CreateIndexModel<AttestationBlob>(Builders<AttestationBlob>.IndexKeys.Ascending(x => x.Digest), new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(AttestationRecord record, byte[] content, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = AttestationDocument.From(record);
|
||||
await _index.ReplaceOneAsync(x => x.PackId == record.PackId && x.Type == record.Type, doc, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var blob = new AttestationBlob { Digest = record.Digest, Content = content };
|
||||
await _blobs.ReplaceOneAsync(x => x.Digest == blob.Digest, blob, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<AttestationRecord?> GetAsync(string packId, string type, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = await _index.Find(x => x.PackId == packId && x.Type == type).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToModel();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AttestationRecord>> ListAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var docs = await _index.Find(x => x.PackId == packId).SortBy(x => x.Type).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return docs.Select(d => d.ToModel()).ToList();
|
||||
}
|
||||
|
||||
public async Task<byte[]?> GetContentAsync(string packId, string type, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var record = await GetAsync(packId, type, cancellationToken).ConfigureAwait(false);
|
||||
if (record is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blob = await _blobs.Find(x => x.Digest == record.Digest).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return blob?.Content;
|
||||
}
|
||||
|
||||
private sealed class AttestationDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string PackId { get; set; } = default!;
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string Type { get; set; } = default!;
|
||||
public string Digest { get; set; } = default!;
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public AttestationRecord ToModel() => new(PackId, TenantId, Type, Digest, CreatedAtUtc, Notes);
|
||||
public static AttestationDocument From(AttestationRecord record) => new()
|
||||
{
|
||||
PackId = record.PackId,
|
||||
TenantId = record.TenantId,
|
||||
Type = record.Type,
|
||||
Digest = record.Digest,
|
||||
CreatedAtUtc = record.CreatedAtUtc,
|
||||
Notes = record.Notes
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class AttestationBlob
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string Digest { get; set; } = default!;
|
||||
public byte[] Content { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoAuditRepository : IAuditRepository
|
||||
{
|
||||
private readonly IMongoCollection<AuditDocument> _collection;
|
||||
|
||||
public MongoAuditRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
_collection = database.GetCollection<AuditDocument>(options.AuditCollection ?? "packs_audit_log");
|
||||
var indexKeys = Builders<AuditDocument>.IndexKeys
|
||||
.Ascending(x => x.TenantId)
|
||||
.Ascending(x => x.PackId)
|
||||
.Ascending(x => x.OccurredAtUtc);
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<AuditDocument>(indexKeys));
|
||||
}
|
||||
|
||||
public async Task AppendAsync(AuditRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = AuditDocument.From(record);
|
||||
await _collection.InsertOneAsync(doc, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AuditRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(tenantId)
|
||||
? Builders<AuditDocument>.Filter.Empty
|
||||
: Builders<AuditDocument>.Filter.Eq(x => x.TenantId, tenantId);
|
||||
|
||||
var docs = await _collection.Find(filter)
|
||||
.SortBy(x => x.OccurredAtUtc)
|
||||
.ThenBy(x => x.PackId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return docs.Select(d => d.ToModel()).ToList();
|
||||
}
|
||||
|
||||
private sealed class AuditDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string? PackId { get; set; }
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string Event { get; set; } = default!;
|
||||
public DateTimeOffset OccurredAtUtc { get; set; }
|
||||
public string? Actor { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public AuditRecord ToModel() => new(PackId, TenantId, Event, OccurredAtUtc, Actor, Notes);
|
||||
|
||||
public static AuditDocument From(AuditRecord record) => new()
|
||||
{
|
||||
PackId = record.PackId,
|
||||
TenantId = record.TenantId,
|
||||
Event = record.Event,
|
||||
OccurredAtUtc = record.OccurredAtUtc,
|
||||
Actor = record.Actor,
|
||||
Notes = record.Notes
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoLifecycleRepository : ILifecycleRepository
|
||||
{
|
||||
private readonly IMongoCollection<LifecycleDocument> _collection;
|
||||
|
||||
public MongoLifecycleRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
_collection = database.GetCollection<LifecycleDocument>(options.LifecycleCollection ?? "packs_lifecycle");
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<LifecycleDocument>(Builders<LifecycleDocument>.IndexKeys.Ascending(x => x.PackId), new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(LifecycleRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = LifecycleDocument.From(record);
|
||||
await _collection.ReplaceOneAsync(x => x.PackId == record.PackId, doc, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<LifecycleRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = await _collection.Find(x => x.PackId == packId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToModel();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<LifecycleRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(tenantId)
|
||||
? Builders<LifecycleDocument>.Filter.Empty
|
||||
: Builders<LifecycleDocument>.Filter.Eq(x => x.TenantId, tenantId);
|
||||
|
||||
var docs = await _collection.Find(filter)
|
||||
.SortBy(x => x.PackId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return docs.Select(d => d.ToModel()).ToList();
|
||||
}
|
||||
|
||||
private sealed class LifecycleDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string PackId { get; set; } = default!;
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string State { get; set; } = default!;
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
|
||||
public LifecycleRecord ToModel() => new(PackId, TenantId, State, Notes, UpdatedAtUtc);
|
||||
public static LifecycleDocument From(LifecycleRecord record) => new()
|
||||
{
|
||||
PackId = record.PackId,
|
||||
TenantId = record.TenantId,
|
||||
State = record.State,
|
||||
Notes = record.Notes,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoMirrorRepository : IMirrorRepository
|
||||
{
|
||||
private readonly IMongoCollection<MirrorDocument> _collection;
|
||||
|
||||
public MongoMirrorRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
_collection = database.GetCollection<MirrorDocument>(options.MirrorCollection ?? "packs_mirrors");
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<MirrorDocument>(Builders<MirrorDocument>.IndexKeys.Ascending(x => x.Id), new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(MirrorSourceRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = MirrorDocument.From(record);
|
||||
await _collection.ReplaceOneAsync(x => x.Id == record.Id, doc, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<MirrorSourceRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(tenantId)
|
||||
? Builders<MirrorDocument>.Filter.Empty
|
||||
: Builders<MirrorDocument>.Filter.Eq(x => x.TenantId, tenantId);
|
||||
|
||||
var docs = await _collection.Find(filter).SortBy(x => x.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return docs.Select(d => d.ToModel()).ToList();
|
||||
}
|
||||
|
||||
public async Task<MirrorSourceRecord?> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = await _collection.Find(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToModel();
|
||||
}
|
||||
|
||||
private sealed class MirrorDocument
|
||||
{
|
||||
public ObjectId InternalId { get; set; }
|
||||
public string Id { get; set; } = default!;
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string Upstream { get; set; } = default!;
|
||||
public bool Enabled { get; set; }
|
||||
public string Status { get; set; } = default!;
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset? LastSuccessfulSyncUtc { get; set; }
|
||||
|
||||
public MirrorSourceRecord ToModel() => new(Id, TenantId, new Uri(Upstream), Enabled, Status, UpdatedAtUtc, Notes, LastSuccessfulSyncUtc);
|
||||
public static MirrorDocument From(MirrorSourceRecord record) => new()
|
||||
{
|
||||
Id = record.Id,
|
||||
TenantId = record.TenantId,
|
||||
Upstream = record.UpstreamUri.ToString(),
|
||||
Enabled = record.Enabled,
|
||||
Status = record.Status,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc,
|
||||
Notes = record.Notes,
|
||||
LastSuccessfulSyncUtc = record.LastSuccessfulSyncUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoPackRepository : IPackRepository
|
||||
{
|
||||
private readonly IMongoCollection<PackDocument> _packs;
|
||||
private readonly IMongoCollection<PackContentDocument> _contents;
|
||||
|
||||
public MongoPackRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
_packs = database.GetCollection<PackDocument>(options.PacksCollection);
|
||||
_contents = database.GetCollection<PackContentDocument>(options.BlobsCollection);
|
||||
|
||||
_packs.Indexes.CreateOne(new CreateIndexModel<PackDocument>(Builders<PackDocument>.IndexKeys.Ascending(x => x.PackId), new CreateIndexOptions { Unique = true }));
|
||||
_packs.Indexes.CreateOne(new CreateIndexModel<PackDocument>(Builders<PackDocument>.IndexKeys.Ascending(x => x.TenantId).Ascending(x => x.Name).Ascending(x => x.Version)));
|
||||
_contents.Indexes.CreateOne(new CreateIndexModel<PackContentDocument>(Builders<PackContentDocument>.IndexKeys.Ascending(x => x.Digest), new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(PackRecord record, byte[] content, byte[]? provenance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
var packDoc = PackDocument.From(record);
|
||||
await _packs.ReplaceOneAsync(x => x.PackId == record.PackId, packDoc, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var blob = new PackContentDocument
|
||||
{
|
||||
Digest = record.Digest,
|
||||
Content = content,
|
||||
ProvenanceDigest = record.ProvenanceDigest,
|
||||
Provenance = provenance
|
||||
};
|
||||
|
||||
await _contents.ReplaceOneAsync(x => x.Digest == record.Digest, blob, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<PackRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = await _packs.Find(x => x.PackId == packId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToModel();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PackRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(tenantId)
|
||||
? Builders<PackDocument>.Filter.Empty
|
||||
: Builders<PackDocument>.Filter.Eq(x => x.TenantId, tenantId);
|
||||
|
||||
var docs = await _packs.Find(filter).SortBy(x => x.TenantId).ThenBy(x => x.Name).ThenBy(x => x.Version).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
return docs.Select(d => d.ToModel()).ToArray();
|
||||
}
|
||||
|
||||
public async Task<byte[]?> GetContentAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pack = await GetAsync(packId, cancellationToken).ConfigureAwait(false);
|
||||
if (pack is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blob = await _contents.Find(x => x.Digest == pack.Digest).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return blob?.Content;
|
||||
}
|
||||
|
||||
public async Task<byte[]?> GetProvenanceAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pack = await GetAsync(packId, cancellationToken).ConfigureAwait(false);
|
||||
if (pack is null || string.IsNullOrWhiteSpace(pack.ProvenanceDigest))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var blob = await _contents.Find(x => x.Digest == pack.Digest).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return blob?.Provenance;
|
||||
}
|
||||
|
||||
private sealed class PackDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string PackId { get; set; } = default!;
|
||||
public string Name { get; set; } = default!;
|
||||
public string Version { get; set; } = default!;
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string Digest { get; set; } = default!;
|
||||
public string? Signature { get; set; }
|
||||
public string? ProvenanceUri { get; set; }
|
||||
public string? ProvenanceDigest { get; set; }
|
||||
public DateTimeOffset CreatedAtUtc { get; set; }
|
||||
public Dictionary<string, string>? Metadata { get; set; }
|
||||
|
||||
public PackRecord ToModel() => new(PackId, Name, Version, TenantId, Digest, Signature, ProvenanceUri, ProvenanceDigest, CreatedAtUtc, Metadata);
|
||||
|
||||
public static PackDocument From(PackRecord model) => new()
|
||||
{
|
||||
PackId = model.PackId,
|
||||
Name = model.Name,
|
||||
Version = model.Version,
|
||||
TenantId = model.TenantId,
|
||||
Digest = model.Digest,
|
||||
Signature = model.Signature,
|
||||
ProvenanceUri = model.ProvenanceUri,
|
||||
ProvenanceDigest = model.ProvenanceDigest,
|
||||
CreatedAtUtc = model.CreatedAtUtc,
|
||||
Metadata = model.Metadata?.ToDictionary(kv => kv.Key, kv => kv.Value)
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class PackContentDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string Digest { get; set; } = default!;
|
||||
public byte[] Content { get; set; } = Array.Empty<byte>();
|
||||
public string? ProvenanceDigest { get; set; }
|
||||
public byte[]? Provenance { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
public sealed class MongoParityRepository : IParityRepository
|
||||
{
|
||||
private readonly IMongoCollection<ParityDocument> _collection;
|
||||
|
||||
public MongoParityRepository(IMongoDatabase database, MongoOptions options)
|
||||
{
|
||||
_collection = database.GetCollection<ParityDocument>(options.ParityCollection ?? "packs_parity_matrix");
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<ParityDocument>(Builders<ParityDocument>.IndexKeys.Ascending(x => x.PackId), new CreateIndexOptions { Unique = true }));
|
||||
}
|
||||
|
||||
public async Task UpsertAsync(ParityRecord record, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = ParityDocument.From(record);
|
||||
await _collection.ReplaceOneAsync(x => x.PackId == record.PackId, doc, new ReplaceOptions { IsUpsert = true }, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ParityRecord?> GetAsync(string packId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var doc = await _collection.Find(x => x.PackId == packId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
return doc?.ToModel();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ParityRecord>> ListAsync(string? tenantId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var filter = string.IsNullOrWhiteSpace(tenantId)
|
||||
? Builders<ParityDocument>.Filter.Empty
|
||||
: Builders<ParityDocument>.Filter.Eq(x => x.TenantId, tenantId);
|
||||
|
||||
var docs = await _collection.Find(filter)
|
||||
.SortBy(x => x.PackId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return docs.Select(d => d.ToModel()).ToList();
|
||||
}
|
||||
|
||||
private sealed class ParityDocument
|
||||
{
|
||||
public ObjectId Id { get; set; }
|
||||
public string PackId { get; set; } = default!;
|
||||
public string TenantId { get; set; } = default!;
|
||||
public string Status { get; set; } = default!;
|
||||
public string? Notes { get; set; }
|
||||
public DateTimeOffset UpdatedAtUtc { get; set; }
|
||||
|
||||
public ParityRecord ToModel() => new(PackId, TenantId, Status, Notes, UpdatedAtUtc);
|
||||
public static ParityDocument From(ParityRecord record) => new()
|
||||
{
|
||||
PackId = record.PackId,
|
||||
TenantId = record.TenantId,
|
||||
Status = record.Status,
|
||||
Notes = record.Notes,
|
||||
UpdatedAtUtc = record.UpdatedAtUtc
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures Mongo collections and indexes exist for packs, blobs, and parity matrix.
|
||||
/// </summary>
|
||||
public sealed class PacksMongoInitializer : IHostedService
|
||||
{
|
||||
private readonly IMongoDatabase _database;
|
||||
private readonly MongoOptions _options;
|
||||
private readonly ILogger<PacksMongoInitializer> _logger;
|
||||
|
||||
public PacksMongoInitializer(IMongoDatabase database, MongoOptions options, ILogger<PacksMongoInitializer> logger)
|
||||
{
|
||||
_database = database ?? throw new ArgumentNullException(nameof(database));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsurePacksIndexAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureBlobsIndexAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureParityMatrixAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureLifecycleAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureAuditAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureAttestationsAsync(cancellationToken).ConfigureAwait(false);
|
||||
await EnsureMirrorsAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
private async Task EnsurePacksIndexAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var packs = _database.GetCollection<BsonDocument>(_options.PacksCollection);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("packId");
|
||||
await packs.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var secondary = Builders<BsonDocument>.IndexKeys.Ascending("tenantId").Ascending("name").Ascending("version");
|
||||
await packs.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(secondary), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task EnsureBlobsIndexAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var blobs = _database.GetCollection<BsonDocument>(_options.BlobsCollection);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("digest");
|
||||
await blobs.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task EnsureParityMatrixAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var parityName = _options.ParityCollection ?? "packs_parity_matrix";
|
||||
var parity = _database.GetCollection<BsonDocument>(parityName);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("packId");
|
||||
await parity.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Mongo collections ensured: {Packs}, {Blobs}, {Parity}", _options.PacksCollection, _options.BlobsCollection, parityName);
|
||||
}
|
||||
|
||||
private async Task EnsureLifecycleAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var lifecycleName = _options.LifecycleCollection ?? "packs_lifecycle";
|
||||
var lifecycle = _database.GetCollection<BsonDocument>(lifecycleName);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("packId");
|
||||
await lifecycle.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Mongo lifecycle collection ensured: {Lifecycle}", lifecycleName);
|
||||
}
|
||||
|
||||
private async Task EnsureAuditAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var auditName = _options.AuditCollection ?? "packs_audit_log";
|
||||
var audit = _database.GetCollection<BsonDocument>(auditName);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys
|
||||
.Ascending("tenantId")
|
||||
.Ascending("packId")
|
||||
.Ascending("occurredAtUtc");
|
||||
await audit.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Mongo audit collection ensured: {Audit}", auditName);
|
||||
}
|
||||
|
||||
private async Task EnsureAttestationsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var attestName = _options.AttestationCollection ?? "packs_attestations";
|
||||
var attest = _database.GetCollection<BsonDocument>(attestName);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("packId").Ascending("type");
|
||||
await attest.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var blobsName = _options.AttestationBlobsCollection ?? "packs_attestation_blobs";
|
||||
var blobs = _database.GetCollection<BsonDocument>(blobsName);
|
||||
var blobIndex = Builders<BsonDocument>.IndexKeys.Ascending("digest");
|
||||
await blobs.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(blobIndex, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Mongo attestation collections ensured: {Attest} / {AttestBlobs}", attestName, blobsName);
|
||||
}
|
||||
|
||||
private async Task EnsureMirrorsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var mirrorName = _options.MirrorCollection ?? "packs_mirrors";
|
||||
var mirrors = _database.GetCollection<BsonDocument>(mirrorName);
|
||||
var indexKeys = Builders<BsonDocument>.IndexKeys.Ascending("id");
|
||||
await mirrors.Indexes.CreateOneAsync(new CreateIndexModel<BsonDocument>(indexKeys, new CreateIndexOptions { Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Mongo mirror collection ensured: {Mirror}", mirrorName);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
|
||||
public sealed class MongoOptions
|
||||
{
|
||||
public string? ConnectionString { get; set; }
|
||||
public string Database { get; set; } = "packs_registry";
|
||||
public string PacksCollection { get; set; } = "packs_index";
|
||||
public string BlobsCollection { get; set; } = "packs_blobs";
|
||||
public string? ParityCollection { get; set; } = "packs_parity_matrix";
|
||||
public string? LifecycleCollection { get; set; } = "packs_lifecycle";
|
||||
public string? AuditCollection { get; set; } = "packs_audit_log";
|
||||
public string? AttestationCollection { get; set; } = "packs_attestations";
|
||||
public string? AttestationBlobsCollection { get; set; } = "packs_attestation_blobs";
|
||||
public string? MirrorCollection { get; set; } = "packs_mirrors";
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
|
||||
|
||||
@@ -3,15 +3,11 @@ using StellaOps.PacksRegistry.Core.Contracts;
|
||||
using StellaOps.PacksRegistry.Core.Models;
|
||||
using StellaOps.PacksRegistry.Core.Services;
|
||||
using StellaOps.PacksRegistry.Infrastructure.FileSystem;
|
||||
using StellaOps.PacksRegistry.Infrastructure.InMemory;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Verification;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Mongo;
|
||||
using StellaOps.PacksRegistry.Infrastructure.Options;
|
||||
using StellaOps.PacksRegistry.WebService;
|
||||
using StellaOps.PacksRegistry.WebService.Contracts;
|
||||
using StellaOps.PacksRegistry.WebService.Options;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using MongoDB.Driver;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -22,32 +18,14 @@ builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
});
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
var dataDir = builder.Configuration.GetValue<string>("PacksRegistry:DataDir");
|
||||
var mongoOptions = builder.Configuration.GetSection("PacksRegistry:Mongo").Get<MongoOptions>() ?? new MongoOptions();
|
||||
mongoOptions.ConnectionString ??= builder.Configuration.GetConnectionString("packs-registry");
|
||||
var dataDir = builder.Configuration.GetValue<string>("PacksRegistry:DataDir") ?? Path.Combine("data", "packs");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(mongoOptions.ConnectionString))
|
||||
{
|
||||
builder.Services.AddSingleton(mongoOptions);
|
||||
builder.Services.AddSingleton<IMongoClient>(_ => new MongoClient(mongoOptions.ConnectionString));
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IMongoClient>().GetDatabase(mongoOptions.Database));
|
||||
builder.Services.AddSingleton<IPackRepository, MongoPackRepository>();
|
||||
builder.Services.AddSingleton<IParityRepository, MongoParityRepository>();
|
||||
builder.Services.AddSingleton<ILifecycleRepository, MongoLifecycleRepository>();
|
||||
builder.Services.AddSingleton<IAuditRepository, MongoAuditRepository>();
|
||||
builder.Services.AddSingleton<IAttestationRepository, MongoAttestationRepository>();
|
||||
builder.Services.AddSingleton<IMirrorRepository, MongoMirrorRepository>();
|
||||
builder.Services.AddHostedService<PacksMongoInitializer>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddSingleton<IPackRepository>(_ => new FilePackRepository(dataDir ?? "data/packs"));
|
||||
builder.Services.AddSingleton<IParityRepository>(_ => new FileParityRepository(dataDir ?? "data/packs"));
|
||||
builder.Services.AddSingleton<ILifecycleRepository>(_ => new FileLifecycleRepository(dataDir ?? "data/packs"));
|
||||
builder.Services.AddSingleton<IAuditRepository>(_ => new FileAuditRepository(dataDir ?? "data/packs"));
|
||||
builder.Services.AddSingleton<IAttestationRepository>(_ => new FileAttestationRepository(dataDir ?? "data/packs"));
|
||||
builder.Services.AddSingleton<IMirrorRepository>(_ => new FileMirrorRepository(dataDir ?? "data/packs"));
|
||||
}
|
||||
builder.Services.AddSingleton<IPackRepository>(_ => new FilePackRepository(dataDir));
|
||||
builder.Services.AddSingleton<IParityRepository>(_ => new FileParityRepository(dataDir));
|
||||
builder.Services.AddSingleton<ILifecycleRepository>(_ => new FileLifecycleRepository(dataDir));
|
||||
builder.Services.AddSingleton<IAuditRepository>(_ => new FileAuditRepository(dataDir));
|
||||
builder.Services.AddSingleton<IAttestationRepository>(_ => new FileAttestationRepository(dataDir));
|
||||
builder.Services.AddSingleton<IMirrorRepository>(_ => new FileMirrorRepository(dataDir));
|
||||
|
||||
var verificationSection = builder.Configuration.GetSection("PacksRegistry:Verification");
|
||||
builder.Services.Configure<VerificationOptions>(verificationSection);
|
||||
|
||||
Reference in New Issue
Block a user