- 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>
134 lines
5.5 KiB
C#
134 lines
5.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|