fix tests. new product advisories enhancements
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ResilientRekorClient.cs
|
||||
// Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
// Task: WORKFLOW-006 - Implement mirror failover
|
||||
// Description: Resilient Rekor client with circuit breaker and mirror failover
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.Core.Resilience;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
/// <summary>
|
||||
/// Resilient Rekor client with circuit breaker and automatic mirror failover.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Flow:
|
||||
/// 1. Try primary backend
|
||||
/// 2. If primary circuit is OPEN and mirror is enabled, try mirror
|
||||
/// 3. If primary fails and circuit is HALF_OPEN, mark failure and try mirror
|
||||
/// 4. Track success/failure for circuit breaker state transitions
|
||||
/// </remarks>
|
||||
public sealed class ResilientRekorClient : IRekorClient, IDisposable
|
||||
{
|
||||
private readonly IRekorClient _innerClient;
|
||||
private readonly IRekorBackendResolver _backendResolver;
|
||||
private readonly CircuitBreaker _primaryCircuitBreaker;
|
||||
private readonly CircuitBreaker? _mirrorCircuitBreaker;
|
||||
private readonly AttestorOptions _options;
|
||||
private readonly ILogger<ResilientRekorClient> _logger;
|
||||
|
||||
public ResilientRekorClient(
|
||||
IRekorClient innerClient,
|
||||
IRekorBackendResolver backendResolver,
|
||||
IOptions<AttestorOptions> options,
|
||||
ILogger<ResilientRekorClient> logger,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_innerClient = innerClient ?? throw new ArgumentNullException(nameof(innerClient));
|
||||
_backendResolver = backendResolver ?? throw new ArgumentNullException(nameof(backendResolver));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
var cbOptions = MapCircuitBreakerOptions(_options.Rekor.CircuitBreaker);
|
||||
var time = timeProvider ?? TimeProvider.System;
|
||||
|
||||
_primaryCircuitBreaker = new CircuitBreaker(
|
||||
"rekor-primary",
|
||||
cbOptions,
|
||||
logger as ILogger<CircuitBreaker>,
|
||||
time);
|
||||
|
||||
_primaryCircuitBreaker.StateChanged += OnPrimaryCircuitStateChanged;
|
||||
|
||||
// Create mirror circuit breaker if mirror is enabled
|
||||
if (_options.Rekor.Mirror.Enabled)
|
||||
{
|
||||
_mirrorCircuitBreaker = new CircuitBreaker(
|
||||
"rekor-mirror",
|
||||
cbOptions,
|
||||
logger as ILogger<CircuitBreaker>,
|
||||
time);
|
||||
|
||||
_mirrorCircuitBreaker.StateChanged += OnMirrorCircuitStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state of the primary circuit breaker.
|
||||
/// </summary>
|
||||
public CircuitState PrimaryCircuitState => _primaryCircuitBreaker.State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state of the mirror circuit breaker.
|
||||
/// </summary>
|
||||
public CircuitState? MirrorCircuitState => _mirrorCircuitBreaker?.State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether requests are currently being routed to the mirror.
|
||||
/// </summary>
|
||||
public bool IsUsingMirror => _options.Rekor.Mirror.Enabled
|
||||
&& _options.Rekor.CircuitBreaker.FailoverToMirrorWhenOpen
|
||||
&& _primaryCircuitBreaker.State == CircuitState.Open
|
||||
&& _mirrorCircuitBreaker?.State != CircuitState.Open;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when failover to mirror occurs.
|
||||
/// </summary>
|
||||
public event Action<string>? FailoverOccurred;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when failback to primary occurs.
|
||||
/// </summary>
|
||||
public event Action<string>? FailbackOccurred;
|
||||
|
||||
public async Task<RekorSubmissionResponse> SubmitAsync(
|
||||
AttestorSubmissionRequest request,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Submissions always go to primary (or resolved backend)
|
||||
// We don't submit to mirrors to avoid duplicates
|
||||
return await ExecuteWithResilienceAsync(
|
||||
async (b, ct) => await _innerClient.SubmitAsync(request, b, ct),
|
||||
backend,
|
||||
"Submit",
|
||||
allowMirror: false, // Never submit to mirror
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RekorProofResponse?> GetProofAsync(
|
||||
string rekorUuid,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await ExecuteWithResilienceAsync(
|
||||
async (b, ct) => await _innerClient.GetProofAsync(rekorUuid, b, ct),
|
||||
backend,
|
||||
"GetProof",
|
||||
allowMirror: true,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RekorInclusionVerificationResult> VerifyInclusionAsync(
|
||||
string rekorUuid,
|
||||
byte[] payloadDigest,
|
||||
RekorBackend backend,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await ExecuteWithResilienceAsync(
|
||||
async (b, ct) => await _innerClient.VerifyInclusionAsync(rekorUuid, payloadDigest, b, ct),
|
||||
backend,
|
||||
"VerifyInclusion",
|
||||
allowMirror: true,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteWithResilienceAsync<T>(
|
||||
Func<RekorBackend, CancellationToken, Task<T>> operation,
|
||||
RekorBackend requestedBackend,
|
||||
string operationName,
|
||||
bool allowMirror,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cbOptions = _options.Rekor.CircuitBreaker;
|
||||
|
||||
// If circuit breaker is disabled, just execute directly
|
||||
if (!cbOptions.Enabled)
|
||||
{
|
||||
return await operation(requestedBackend, cancellationToken);
|
||||
}
|
||||
|
||||
// Check if we should use mirror due to primary circuit being open
|
||||
if (allowMirror && ShouldUseMirror())
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Primary circuit is OPEN, routing {Operation} to mirror",
|
||||
operationName);
|
||||
|
||||
var mirrorBackend = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirrorBackend != null && _mirrorCircuitBreaker!.AllowRequest())
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await operation(mirrorBackend, cancellationToken);
|
||||
_mirrorCircuitBreaker.RecordSuccess();
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (IsTransientException(ex))
|
||||
{
|
||||
_mirrorCircuitBreaker.RecordFailure();
|
||||
_logger.LogWarning(ex,
|
||||
"Mirror {Operation} failed, no fallback available",
|
||||
operationName);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try primary
|
||||
if (_primaryCircuitBreaker.AllowRequest())
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await operation(requestedBackend, cancellationToken);
|
||||
_primaryCircuitBreaker.RecordSuccess();
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (IsTransientException(ex))
|
||||
{
|
||||
_primaryCircuitBreaker.RecordFailure();
|
||||
|
||||
// Try mirror on primary failure (if allowed and available)
|
||||
if (allowMirror && cbOptions.FailoverToMirrorWhenOpen)
|
||||
{
|
||||
var mirrorBackend = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirrorBackend != null && _mirrorCircuitBreaker?.AllowRequest() == true)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"Primary {Operation} failed, failing over to mirror",
|
||||
operationName);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await operation(mirrorBackend, cancellationToken);
|
||||
_mirrorCircuitBreaker.RecordSuccess();
|
||||
OnFailover("immediate-failover");
|
||||
return result;
|
||||
}
|
||||
catch (Exception mirrorEx) when (IsTransientException(mirrorEx))
|
||||
{
|
||||
_mirrorCircuitBreaker.RecordFailure();
|
||||
_logger.LogWarning(mirrorEx,
|
||||
"Mirror {Operation} also failed",
|
||||
operationName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Primary circuit is open, check for mirror
|
||||
if (allowMirror && cbOptions.FailoverToMirrorWhenOpen)
|
||||
{
|
||||
var mirrorBackend = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirrorBackend != null && _mirrorCircuitBreaker?.AllowRequest() == true)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Primary circuit OPEN, using mirror for {Operation}",
|
||||
operationName);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await operation(mirrorBackend, cancellationToken);
|
||||
_mirrorCircuitBreaker.RecordSuccess();
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (IsTransientException(ex))
|
||||
{
|
||||
_mirrorCircuitBreaker.RecordFailure();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new CircuitBreakerOpenException(
|
||||
_primaryCircuitBreaker.Name,
|
||||
_primaryCircuitBreaker.State);
|
||||
}
|
||||
|
||||
private bool ShouldUseMirror()
|
||||
{
|
||||
return _options.Rekor.Mirror.Enabled
|
||||
&& _options.Rekor.CircuitBreaker.FailoverToMirrorWhenOpen
|
||||
&& _primaryCircuitBreaker.State == CircuitState.Open
|
||||
&& _mirrorCircuitBreaker?.State != CircuitState.Open;
|
||||
}
|
||||
|
||||
private async Task<RekorBackend?> GetMirrorBackendAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_options.Rekor.Mirror.Enabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await _backendResolver.GetMirrorBackendAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void OnPrimaryCircuitStateChanged(CircuitState oldState, CircuitState newState)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Primary Rekor circuit breaker: {OldState} -> {NewState}",
|
||||
oldState, newState);
|
||||
|
||||
if (newState == CircuitState.Open && _options.Rekor.Mirror.Enabled)
|
||||
{
|
||||
OnFailover("circuit-open");
|
||||
}
|
||||
else if (oldState == CircuitState.Open && newState == CircuitState.Closed)
|
||||
{
|
||||
OnFailback("circuit-closed");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMirrorCircuitStateChanged(CircuitState oldState, CircuitState newState)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Mirror Rekor circuit breaker: {OldState} -> {NewState}",
|
||||
oldState, newState);
|
||||
}
|
||||
|
||||
private void OnFailover(string reason)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Rekor failover to mirror activated: {Reason}",
|
||||
reason);
|
||||
FailoverOccurred?.Invoke(reason);
|
||||
}
|
||||
|
||||
private void OnFailback(string reason)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Rekor failback to primary activated: {Reason}",
|
||||
reason);
|
||||
FailbackOccurred?.Invoke(reason);
|
||||
}
|
||||
|
||||
private static CircuitBreakerOptions MapCircuitBreakerOptions(
|
||||
AttestorOptions.RekorCircuitBreakerOptions options)
|
||||
{
|
||||
return new CircuitBreakerOptions
|
||||
{
|
||||
Enabled = options.Enabled,
|
||||
FailureThreshold = options.FailureThreshold,
|
||||
SuccessThreshold = options.SuccessThreshold,
|
||||
OpenDurationSeconds = options.OpenDurationSeconds,
|
||||
FailureWindowSeconds = options.FailureWindowSeconds,
|
||||
HalfOpenMaxRequests = options.HalfOpenMaxRequests,
|
||||
UseCacheWhenOpen = options.UseCacheWhenOpen,
|
||||
FailoverToMirrorWhenOpen = options.FailoverToMirrorWhenOpen
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsTransientException(Exception ex)
|
||||
{
|
||||
return ex is HttpRequestException
|
||||
or TaskCanceledException
|
||||
or TimeoutException
|
||||
or OperationCanceledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets both circuit breakers to closed state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_primaryCircuitBreaker.Reset();
|
||||
_mirrorCircuitBreaker?.Reset();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_primaryCircuitBreaker.StateChanged -= OnPrimaryCircuitStateChanged;
|
||||
_primaryCircuitBreaker.Dispose();
|
||||
|
||||
if (_mirrorCircuitBreaker != null)
|
||||
{
|
||||
_mirrorCircuitBreaker.StateChanged -= OnMirrorCircuitStateChanged;
|
||||
_mirrorCircuitBreaker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// ServiceMapAwareRekorBackendResolver.cs
|
||||
// Sprint: SPRINT_20260125_002_Attestor_trust_automation
|
||||
// Task: PROXY-007 - Integrate service map with HttpRekorClient
|
||||
// Description: Resolves Rekor backends using TUF service map with configuration fallback
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
using StellaOps.Attestor.Core.Rekor;
|
||||
using StellaOps.Attestor.TrustRepo;
|
||||
using StellaOps.Attestor.TrustRepo.Models;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure.Rekor;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves Rekor backends using TUF service map for dynamic endpoint discovery,
|
||||
/// with fallback to static configuration when service map is unavailable.
|
||||
/// </summary>
|
||||
internal sealed class ServiceMapAwareRekorBackendResolver : IRekorBackendResolver
|
||||
{
|
||||
private readonly ISigstoreServiceMapLoader _serviceMapLoader;
|
||||
private readonly IOptions<AttestorOptions> _options;
|
||||
private readonly ILogger<ServiceMapAwareRekorBackendResolver> _logger;
|
||||
private readonly bool _serviceMapEnabled;
|
||||
|
||||
// Cached backend from service map
|
||||
private RekorBackend? _cachedServiceMapBackend;
|
||||
private DateTimeOffset? _cachedAt;
|
||||
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);
|
||||
private readonly SemaphoreSlim _cacheLock = new(1, 1);
|
||||
|
||||
public ServiceMapAwareRekorBackendResolver(
|
||||
ISigstoreServiceMapLoader serviceMapLoader,
|
||||
IOptions<AttestorOptions> options,
|
||||
ILogger<ServiceMapAwareRekorBackendResolver> logger)
|
||||
{
|
||||
_serviceMapLoader = serviceMapLoader ?? throw new ArgumentNullException(nameof(serviceMapLoader));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// Service map is enabled if TrustRepo is configured
|
||||
_serviceMapEnabled = options.Value.TrustRepo?.Enabled ?? false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsServiceMapEnabled => _serviceMapEnabled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RekorBackend> GetPrimaryBackendAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Try service map first if enabled
|
||||
if (_serviceMapEnabled)
|
||||
{
|
||||
var serviceMapBackend = await TryGetServiceMapBackendAsync(cancellationToken);
|
||||
if (serviceMapBackend != null)
|
||||
{
|
||||
_logger.LogDebug("Using Rekor backend from TUF service map: {Url}", serviceMapBackend.Url);
|
||||
return serviceMapBackend;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Service map unavailable, falling back to configuration");
|
||||
}
|
||||
|
||||
// Fallback to configuration
|
||||
return RekorBackendResolver.ResolveBackend(_options.Value, "primary", allowFallbackToPrimary: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<RekorBackend?> GetMirrorBackendAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var opts = _options.Value;
|
||||
|
||||
if (!opts.Rekor.Mirror.Enabled || string.IsNullOrWhiteSpace(opts.Rekor.Mirror.Url))
|
||||
{
|
||||
return Task.FromResult<RekorBackend?>(null);
|
||||
}
|
||||
|
||||
var mirror = RekorBackendResolver.ResolveBackend(opts, "mirror", allowFallbackToPrimary: false);
|
||||
return Task.FromResult<RekorBackend?>(mirror);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<RekorBackend> ResolveBackendAsync(string? backendName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = string.IsNullOrWhiteSpace(backendName)
|
||||
? "primary"
|
||||
: backendName.Trim().ToLowerInvariant();
|
||||
|
||||
if (normalized == "primary")
|
||||
{
|
||||
return await GetPrimaryBackendAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (normalized == "mirror")
|
||||
{
|
||||
var mirror = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirror == null)
|
||||
{
|
||||
throw new InvalidOperationException("Mirror backend is not configured");
|
||||
}
|
||||
return mirror;
|
||||
}
|
||||
|
||||
// Unknown backend name - try configuration fallback
|
||||
return RekorBackendResolver.ResolveBackend(_options.Value, backendName, allowFallbackToPrimary: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<RekorBackend>> GetAllBackendsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backends = new List<RekorBackend>();
|
||||
|
||||
// Add primary
|
||||
backends.Add(await GetPrimaryBackendAsync(cancellationToken));
|
||||
|
||||
// Add mirror if configured
|
||||
var mirror = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirror != null)
|
||||
{
|
||||
backends.Add(mirror);
|
||||
}
|
||||
|
||||
return backends;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get Rekor backend from TUF service map.
|
||||
/// </summary>
|
||||
private async Task<RekorBackend?> TryGetServiceMapBackendAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Check cache first
|
||||
if (_cachedServiceMapBackend != null && _cachedAt != null)
|
||||
{
|
||||
var age = DateTimeOffset.UtcNow - _cachedAt.Value;
|
||||
if (age < _cacheDuration)
|
||||
{
|
||||
return _cachedServiceMapBackend;
|
||||
}
|
||||
}
|
||||
|
||||
await _cacheLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
// Double-check after acquiring lock
|
||||
if (_cachedServiceMapBackend != null && _cachedAt != null)
|
||||
{
|
||||
var age = DateTimeOffset.UtcNow - _cachedAt.Value;
|
||||
if (age < _cacheDuration)
|
||||
{
|
||||
return _cachedServiceMapBackend;
|
||||
}
|
||||
}
|
||||
|
||||
return await LoadFromServiceMapAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cacheLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads Rekor backend from service map.
|
||||
/// </summary>
|
||||
private async Task<RekorBackend?> LoadFromServiceMapAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var serviceMap = await _serviceMapLoader.GetServiceMapAsync(cancellationToken);
|
||||
if (serviceMap?.Rekor == null || string.IsNullOrEmpty(serviceMap.Rekor.Url))
|
||||
{
|
||||
_logger.LogDebug("Service map does not contain Rekor configuration");
|
||||
return null;
|
||||
}
|
||||
|
||||
var rekor = serviceMap.Rekor;
|
||||
var opts = _options.Value;
|
||||
|
||||
// Build backend from service map, using config for non-mapped settings
|
||||
var backend = new RekorBackend
|
||||
{
|
||||
Name = "primary-servicemap",
|
||||
Url = new Uri(rekor.Url, UriKind.Absolute),
|
||||
Version = ParseLogVersion(opts.Rekor.Primary.Version),
|
||||
TileBaseUrl = !string.IsNullOrEmpty(rekor.TileBaseUrl)
|
||||
? new Uri(rekor.TileBaseUrl, UriKind.Absolute)
|
||||
: null,
|
||||
LogId = !string.IsNullOrEmpty(rekor.LogId)
|
||||
? rekor.LogId
|
||||
: opts.Rekor.Primary.LogId,
|
||||
ProofTimeout = TimeSpan.FromMilliseconds(opts.Rekor.Primary.ProofTimeoutMs),
|
||||
PollInterval = TimeSpan.FromMilliseconds(opts.Rekor.Primary.PollIntervalMs),
|
||||
MaxAttempts = opts.Rekor.Primary.MaxAttempts
|
||||
};
|
||||
|
||||
_cachedServiceMapBackend = backend;
|
||||
_cachedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Loaded Rekor endpoint from TUF service map v{Version}: {Url}",
|
||||
serviceMap.Version,
|
||||
backend.Url);
|
||||
|
||||
return backend;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load Rekor backend from service map");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the log version string to the enum value.
|
||||
/// </summary>
|
||||
private static RekorLogVersion ParseLogVersion(string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return RekorLogVersion.Auto;
|
||||
}
|
||||
|
||||
return version.Trim().ToUpperInvariant() switch
|
||||
{
|
||||
"AUTO" => RekorLogVersion.Auto,
|
||||
"V2" or "2" => RekorLogVersion.V2,
|
||||
_ => RekorLogVersion.Auto
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple resolver that uses only static configuration (no service map).
|
||||
/// </summary>
|
||||
internal sealed class ConfiguredRekorBackendResolver : IRekorBackendResolver
|
||||
{
|
||||
private readonly IOptions<AttestorOptions> _options;
|
||||
|
||||
public ConfiguredRekorBackendResolver(IOptions<AttestorOptions> options)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public bool IsServiceMapEnabled => false;
|
||||
|
||||
public Task<RekorBackend> GetPrimaryBackendAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(RekorBackendResolver.ResolveBackend(_options.Value, "primary", true));
|
||||
}
|
||||
|
||||
public Task<RekorBackend?> GetMirrorBackendAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var opts = _options.Value;
|
||||
if (!opts.Rekor.Mirror.Enabled || string.IsNullOrWhiteSpace(opts.Rekor.Mirror.Url))
|
||||
{
|
||||
return Task.FromResult<RekorBackend?>(null);
|
||||
}
|
||||
|
||||
var mirror = RekorBackendResolver.ResolveBackend(opts, "mirror", false);
|
||||
return Task.FromResult<RekorBackend?>(mirror);
|
||||
}
|
||||
|
||||
public Task<RekorBackend> ResolveBackendAsync(string? backendName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(RekorBackendResolver.ResolveBackend(_options.Value, backendName, true));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<RekorBackend>> GetAllBackendsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var backends = new List<RekorBackend>
|
||||
{
|
||||
await GetPrimaryBackendAsync(cancellationToken)
|
||||
};
|
||||
|
||||
var mirror = await GetMirrorBackendAsync(cancellationToken);
|
||||
if (mirror != null)
|
||||
{
|
||||
backends.Add(mirror);
|
||||
}
|
||||
|
||||
return backends;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ using StellaOps.Attestor.Core.InToto;
|
||||
using StellaOps.Attestor.Core.InToto.Layout;
|
||||
using StellaOps.Attestor.Infrastructure.InToto;
|
||||
using StellaOps.Attestor.Verify;
|
||||
using StellaOps.Attestor.TrustRepo;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Attestor.Infrastructure;
|
||||
@@ -96,6 +97,27 @@ public static class ServiceCollectionExtensions
|
||||
});
|
||||
services.AddSingleton<IRekorClient>(sp => sp.GetRequiredService<HttpRekorClient>());
|
||||
|
||||
// Register Rekor backend resolver with service map support
|
||||
// Sprint: SPRINT_20260125_002 - PROXY-007
|
||||
services.AddSingleton<IRekorBackendResolver>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<AttestorOptions>>().Value;
|
||||
|
||||
// If TrustRepo integration is enabled, use service map-aware resolver
|
||||
if (options.TrustRepo?.Enabled == true)
|
||||
{
|
||||
var serviceMapLoader = sp.GetRequiredService<ISigstoreServiceMapLoader>();
|
||||
var logger = sp.GetRequiredService<ILogger<ServiceMapAwareRekorBackendResolver>>();
|
||||
return new ServiceMapAwareRekorBackendResolver(
|
||||
serviceMapLoader,
|
||||
sp.GetRequiredService<IOptions<AttestorOptions>>(),
|
||||
logger);
|
||||
}
|
||||
|
||||
// Otherwise, use static configuration resolver
|
||||
return new ConfiguredRekorBackendResolver(sp.GetRequiredService<IOptions<AttestorOptions>>());
|
||||
});
|
||||
|
||||
// Rekor v2 tile-based client for Sunlight/tile log format
|
||||
services.AddHttpClient<HttpRekorTileClient>((sp, client) =>
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\..\Router/__Libraries/StellaOps.Messaging\StellaOps.Messaging.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Attestor.TrustRepo\StellaOps.Attestor.TrustRepo.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
|
||||
Reference in New Issue
Block a user