feat(crypto): extract crypto providers to overlay compose files + health probe API
- Extract smremote to docker-compose.crypto-provider.smremote.yml - Rename cryptopro/crypto-sim compose files for consistent naming - Add crypto provider health probe endpoint (CP-001) - Add tenant crypto provider preferences API + migration (CP-002) - Update docs and compliance env examples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Platform.WebService.Contracts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Platform.WebService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Probes known crypto provider health endpoints and returns aggregated status.
|
||||
/// Providers are defined as a static seed list; unreachable providers return "unreachable"
|
||||
/// status (never a 500).
|
||||
/// </summary>
|
||||
public sealed class CryptoProviderHealthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Well-known crypto provider definitions. These match the compose overlays
|
||||
/// in <c>devops/compose/docker-compose.crypto-provider.*.yml</c>.
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyList<CryptoProviderDefinition> KnownProviders =
|
||||
[
|
||||
new CryptoProviderDefinition(
|
||||
Id: "smremote",
|
||||
Name: "SmRemote (SM2/SM3/SM4)",
|
||||
HealthEndpoint: "http://smremote.stella-ops.local/health",
|
||||
ComposeOverlay: "docker-compose.crypto-provider.smremote.yml",
|
||||
StartCommand: "docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.smremote.yml up -d smremote"),
|
||||
|
||||
new CryptoProviderDefinition(
|
||||
Id: "cryptopro",
|
||||
Name: "CryptoPro CSP (GOST)",
|
||||
HealthEndpoint: "http://cryptopro-csp:8080/health",
|
||||
ComposeOverlay: "docker-compose.crypto-provider.cryptopro.yml",
|
||||
StartCommand: "CRYPTOPRO_ACCEPT_EULA=1 docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.cryptopro.yml up -d cryptopro-csp"),
|
||||
|
||||
new CryptoProviderDefinition(
|
||||
Id: "crypto-sim",
|
||||
Name: "Crypto Simulator (dev/test)",
|
||||
HealthEndpoint: "http://sim-crypto:8080/keys",
|
||||
ComposeOverlay: "docker-compose.crypto-provider.crypto-sim.yml",
|
||||
StartCommand: "docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.crypto-sim.yml up -d sim-crypto"),
|
||||
];
|
||||
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
private readonly TimeProvider timeProvider;
|
||||
private readonly ILogger<CryptoProviderHealthService> logger;
|
||||
|
||||
public CryptoProviderHealthService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<CryptoProviderHealthService> logger)
|
||||
{
|
||||
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the static list of known crypto provider definitions.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CryptoProviderDefinition> GetKnownProviders() => KnownProviders;
|
||||
|
||||
/// <summary>
|
||||
/// Probes all known crypto providers concurrently and returns health status for each.
|
||||
/// </summary>
|
||||
public async Task<CryptoProviderHealthResponse> ProbeAllAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var tasks = KnownProviders.Select(provider => ProbeProviderAsync(provider, now, cancellationToken));
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
return new CryptoProviderHealthResponse(
|
||||
Providers: results,
|
||||
CheckedAt: now);
|
||||
}
|
||||
|
||||
private async Task<CryptoProviderHealthStatus> ProbeProviderAsync(
|
||||
CryptoProviderDefinition definition,
|
||||
DateTimeOffset checkedAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var client = httpClientFactory.CreateClient("CryptoProviderProbe");
|
||||
|
||||
try
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, definition.HealthEndpoint);
|
||||
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
stopwatch.Stop();
|
||||
var latencyMs = Math.Round(stopwatch.Elapsed.TotalMilliseconds, 1);
|
||||
|
||||
var status = response.IsSuccessStatusCode ? "running" : "degraded";
|
||||
|
||||
logger.LogDebug(
|
||||
"Crypto provider {ProviderId} probe: HTTP {StatusCode} in {LatencyMs}ms",
|
||||
definition.Id, (int)response.StatusCode, latencyMs);
|
||||
|
||||
return new CryptoProviderHealthStatus(
|
||||
Id: definition.Id,
|
||||
Name: definition.Name,
|
||||
Status: status,
|
||||
LatencyMs: latencyMs,
|
||||
HealthEndpoint: definition.HealthEndpoint,
|
||||
ComposeOverlay: definition.ComposeOverlay,
|
||||
StartCommand: definition.StartCommand,
|
||||
CheckedAt: checkedAt);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug(
|
||||
ex,
|
||||
"Crypto provider {ProviderId} unreachable at {Endpoint}",
|
||||
definition.Id, definition.HealthEndpoint);
|
||||
|
||||
return new CryptoProviderHealthStatus(
|
||||
Id: definition.Id,
|
||||
Name: definition.Name,
|
||||
Status: "unreachable",
|
||||
LatencyMs: null,
|
||||
HealthEndpoint: definition.HealthEndpoint,
|
||||
ComposeOverlay: definition.ComposeOverlay,
|
||||
StartCommand: definition.StartCommand,
|
||||
CheckedAt: checkedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user