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

This commit is contained in:
StellaOps Bot
2025-12-12 09:35:37 +02:00
parent ce5ec9c158
commit efaf3cb789
238 changed files with 146274 additions and 5767 deletions

View File

@@ -27,4 +27,4 @@ Host signed Task Pack bundles with provenance and RBAC for Epic12. 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).

View File

@@ -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!;
}
}

View File

@@ -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
};
}
}

View File

@@ -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
};
}
}

View File

@@ -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
};
}
}

View File

@@ -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; }
}
}

View File

@@ -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
};
}
}

View File

@@ -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);
}
}

View File

@@ -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";
}

View File

@@ -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" />

View File

@@ -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);