203 lines
7.6 KiB
C#
203 lines
7.6 KiB
C#
// -----------------------------------------------------------------------------
|
|
// ServiceCollectionExtensions.cs
|
|
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
|
// Tasks: AS-002, AS-003 - Service registration
|
|
// Description: DI registration for artifact store services
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Artifact.Core;
|
|
using StellaOps.Infrastructure.Postgres.Options;
|
|
|
|
namespace StellaOps.Artifact.Infrastructure;
|
|
|
|
/// <summary>
|
|
/// Extension methods for registering artifact store services.
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Adds unified artifact store services with S3 backend.
|
|
/// </summary>
|
|
/// <param name="services">Service collection.</param>
|
|
/// <param name="configuration">Configuration root.</param>
|
|
/// <param name="sectionName">Configuration section for options.</param>
|
|
/// <returns>Service collection for chaining.</returns>
|
|
public static IServiceCollection AddUnifiedArtifactStore(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration,
|
|
string sectionName = "ArtifactStore")
|
|
{
|
|
// Configure S3 store options
|
|
services.Configure<S3UnifiedArtifactStoreOptions>(configuration.GetSection($"{sectionName}:S3"));
|
|
|
|
// Configure PostgreSQL options for index
|
|
services.Configure<PostgresOptions>("Artifact", configuration.GetSection($"{sectionName}:Postgres"));
|
|
|
|
// Register data source
|
|
services.AddSingleton<ArtifactDataSource>(sp =>
|
|
{
|
|
var options = sp.GetRequiredService<IOptionsSnapshot<PostgresOptions>>().Get("Artifact");
|
|
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<ArtifactDataSource>>();
|
|
return new ArtifactDataSource(Options.Create(options), logger);
|
|
});
|
|
|
|
// Register core services
|
|
services.AddSingleton<ICycloneDxExtractor, CycloneDxExtractor>();
|
|
|
|
// Register index repository
|
|
services.AddScoped<IArtifactIndexRepository>(sp =>
|
|
{
|
|
var dataSource = sp.GetRequiredService<ArtifactDataSource>();
|
|
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<PostgresArtifactIndexRepository>>();
|
|
// TODO: Get tenant ID from context
|
|
return new PostgresArtifactIndexRepository(dataSource, logger, "default");
|
|
});
|
|
|
|
// Register S3 artifact store
|
|
services.AddScoped<IArtifactStore, S3UnifiedArtifactStore>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds unified artifact store with in-memory backend (for testing).
|
|
/// </summary>
|
|
/// <param name="services">Service collection.</param>
|
|
/// <returns>Service collection for chaining.</returns>
|
|
public static IServiceCollection AddInMemoryArtifactStore(this IServiceCollection services)
|
|
{
|
|
services.AddSingleton<ICycloneDxExtractor, CycloneDxExtractor>();
|
|
services.AddSingleton<IArtifactIndexRepository, InMemoryArtifactIndexRepository>();
|
|
services.AddSingleton<IArtifactStore, InMemoryArtifactStore>();
|
|
|
|
return services;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds artifact migration services.
|
|
/// </summary>
|
|
/// <param name="services">Service collection.</param>
|
|
/// <param name="configure">Options configuration.</param>
|
|
/// <returns>Service collection for chaining.</returns>
|
|
public static IServiceCollection AddArtifactMigration(
|
|
this IServiceCollection services,
|
|
Action<ArtifactMigrationOptions>? configure = null)
|
|
{
|
|
var options = new ArtifactMigrationOptions();
|
|
configure?.Invoke(options);
|
|
services.AddSingleton(options);
|
|
|
|
services.AddScoped<ArtifactMigrationService>();
|
|
|
|
return services;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// In-memory artifact store for testing.
|
|
/// </summary>
|
|
public sealed class InMemoryArtifactStore : IArtifactStore
|
|
{
|
|
private readonly Dictionary<string, (byte[] Content, ArtifactMetadata Metadata)> _artifacts = new();
|
|
private readonly object _lock = new();
|
|
|
|
public Task<ArtifactStoreResult> StoreAsync(ArtifactStoreRequest request, CancellationToken ct = default)
|
|
{
|
|
var key = $"{request.BomRef}/{request.SerialNumber}/{request.ArtifactId}";
|
|
using var ms = new MemoryStream();
|
|
request.Content.CopyTo(ms);
|
|
var content = ms.ToArray();
|
|
|
|
using var sha = System.Security.Cryptography.SHA256.Create();
|
|
var hash = sha.ComputeHash(content);
|
|
var sha256 = Convert.ToHexStringLower(hash);
|
|
|
|
var metadata = new ArtifactMetadata
|
|
{
|
|
StorageKey = key,
|
|
BomRef = request.BomRef,
|
|
SerialNumber = request.SerialNumber,
|
|
ArtifactId = request.ArtifactId,
|
|
ContentType = request.ContentType,
|
|
SizeBytes = content.Length,
|
|
Sha256 = sha256,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
Type = request.Type,
|
|
TenantId = request.TenantId
|
|
};
|
|
|
|
lock (_lock)
|
|
{
|
|
var wasCreated = !_artifacts.ContainsKey(key);
|
|
_artifacts[key] = (content, metadata);
|
|
return Task.FromResult(ArtifactStoreResult.Succeeded(key, sha256, content.Length, wasCreated));
|
|
}
|
|
}
|
|
|
|
public Task<ArtifactReadResult> ReadAsync(string bomRef, string? serialNumber, string? artifactId, CancellationToken ct = default)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var matching = _artifacts
|
|
.Where(kvp => kvp.Value.Metadata.BomRef == bomRef)
|
|
.Where(kvp => serialNumber == null || kvp.Value.Metadata.SerialNumber == serialNumber)
|
|
.Where(kvp => artifactId == null || kvp.Value.Metadata.ArtifactId == artifactId)
|
|
.FirstOrDefault();
|
|
|
|
if (matching.Value.Content == null)
|
|
{
|
|
return Task.FromResult(ArtifactReadResult.NotFound());
|
|
}
|
|
|
|
return Task.FromResult(ArtifactReadResult.Succeeded(
|
|
new MemoryStream(matching.Value.Content),
|
|
matching.Value.Metadata));
|
|
}
|
|
}
|
|
|
|
public Task<IReadOnlyList<ArtifactMetadata>> ListAsync(string bomRef, string? serialNumber = null, CancellationToken ct = default)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
var result = _artifacts.Values
|
|
.Where(x => x.Metadata.BomRef == bomRef)
|
|
.Where(x => serialNumber == null || x.Metadata.SerialNumber == serialNumber)
|
|
.Select(x => x.Metadata)
|
|
.ToList();
|
|
|
|
return Task.FromResult<IReadOnlyList<ArtifactMetadata>>(result);
|
|
}
|
|
}
|
|
|
|
public Task<bool> ExistsAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
|
|
{
|
|
var key = $"{bomRef}/{serialNumber}/{artifactId}";
|
|
lock (_lock)
|
|
{
|
|
return Task.FromResult(_artifacts.ContainsKey(key));
|
|
}
|
|
}
|
|
|
|
public Task<ArtifactMetadata?> GetMetadataAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
|
|
{
|
|
var key = $"{bomRef}/{serialNumber}/{artifactId}";
|
|
lock (_lock)
|
|
{
|
|
return Task.FromResult(_artifacts.TryGetValue(key, out var entry) ? entry.Metadata : null);
|
|
}
|
|
}
|
|
|
|
public Task<bool> DeleteAsync(string bomRef, string serialNumber, string artifactId, CancellationToken ct = default)
|
|
{
|
|
var key = $"{bomRef}/{serialNumber}/{artifactId}";
|
|
lock (_lock)
|
|
{
|
|
return Task.FromResult(_artifacts.Remove(key));
|
|
}
|
|
}
|
|
}
|