Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.TaskRunner.Core.AirGap;
|
||||
|
||||
namespace StellaOps.TaskRunner.Infrastructure.AirGap;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client implementation for retrieving air-gap status from the AirGap controller.
|
||||
/// </summary>
|
||||
public sealed class HttpAirGapStatusProvider : IAirGapStatusProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IOptions<AirGapStatusProviderOptions> _options;
|
||||
private readonly ILogger<HttpAirGapStatusProvider> _logger;
|
||||
|
||||
public HttpAirGapStatusProvider(
|
||||
HttpClient httpClient,
|
||||
IOptions<AirGapStatusProviderOptions> options,
|
||||
ILogger<HttpAirGapStatusProvider> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SealedModeStatus> GetStatusAsync(
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var options = _options.Value;
|
||||
var url = string.IsNullOrWhiteSpace(tenantId)
|
||||
? options.StatusEndpoint
|
||||
: $"{options.StatusEndpoint}?tenantId={Uri.EscapeDataString(tenantId)}";
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetFromJsonAsync<AirGapStatusDto>(
|
||||
url,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
_logger.LogWarning("AirGap controller returned null response.");
|
||||
return SealedModeStatus.Unavailable();
|
||||
}
|
||||
|
||||
return MapToSealedModeStatus(response);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to connect to AirGap controller at {Url}.", url);
|
||||
|
||||
if (options.UseHeuristicFallback)
|
||||
{
|
||||
return await GetStatusFromHeuristicsAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return SealedModeStatus.Unavailable();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error getting air-gap status.");
|
||||
return SealedModeStatus.Unavailable();
|
||||
}
|
||||
}
|
||||
|
||||
private static SealedModeStatus MapToSealedModeStatus(AirGapStatusDto dto)
|
||||
{
|
||||
TimeAnchorInfo? timeAnchor = null;
|
||||
if (dto.TimeAnchor is not null)
|
||||
{
|
||||
timeAnchor = new TimeAnchorInfo(
|
||||
dto.TimeAnchor.Timestamp,
|
||||
dto.TimeAnchor.Signature,
|
||||
dto.TimeAnchor.Valid,
|
||||
dto.TimeAnchor.ExpiresAt);
|
||||
}
|
||||
|
||||
return new SealedModeStatus(
|
||||
Sealed: dto.Sealed,
|
||||
Mode: dto.Sealed ? "sealed" : "unsealed",
|
||||
SealedAt: dto.SealedAt,
|
||||
SealedBy: dto.SealedBy,
|
||||
BundleVersion: dto.BundleVersion,
|
||||
BundleDigest: dto.BundleDigest,
|
||||
LastAdvisoryUpdate: dto.LastAdvisoryUpdate,
|
||||
AdvisoryStalenessHours: dto.AdvisoryStalenessHours,
|
||||
TimeAnchor: timeAnchor,
|
||||
EgressBlocked: dto.EgressBlocked,
|
||||
NetworkPolicy: dto.NetworkPolicy);
|
||||
}
|
||||
|
||||
private async Task<SealedModeStatus> GetStatusFromHeuristicsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Using heuristic detection for sealed mode status.");
|
||||
|
||||
var score = 0.0;
|
||||
var weights = 0.0;
|
||||
|
||||
// Check AIRGAP_MODE environment variable (high weight)
|
||||
var airgapMode = Environment.GetEnvironmentVariable("AIRGAP_MODE");
|
||||
if (string.Equals(airgapMode, "sealed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 0.3;
|
||||
}
|
||||
weights += 0.3;
|
||||
|
||||
// Check for sealed file marker (medium weight)
|
||||
var sealedMarkerPath = _options.Value.SealedMarkerPath;
|
||||
if (!string.IsNullOrWhiteSpace(sealedMarkerPath) && File.Exists(sealedMarkerPath))
|
||||
{
|
||||
score += 0.2;
|
||||
}
|
||||
weights += 0.2;
|
||||
|
||||
// Check network connectivity (high weight)
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(2));
|
||||
|
||||
var testResponse = await _httpClient.GetAsync(
|
||||
_options.Value.ConnectivityTestUrl,
|
||||
cts.Token).ConfigureAwait(false);
|
||||
|
||||
// If we can reach external network, likely not sealed
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Network blocked, likely sealed
|
||||
score += 0.3;
|
||||
}
|
||||
weights += 0.3;
|
||||
|
||||
// Check for local registry configuration (low weight)
|
||||
var registryEnv = Environment.GetEnvironmentVariable("CONTAINER_REGISTRY");
|
||||
if (!string.IsNullOrWhiteSpace(registryEnv) &&
|
||||
(registryEnv.Contains("localhost", StringComparison.OrdinalIgnoreCase) ||
|
||||
registryEnv.Contains("127.0.0.1", StringComparison.Ordinal)))
|
||||
{
|
||||
score += 0.1;
|
||||
}
|
||||
weights += 0.1;
|
||||
|
||||
// Check proxy settings (low weight)
|
||||
var httpProxy = Environment.GetEnvironmentVariable("HTTP_PROXY") ??
|
||||
Environment.GetEnvironmentVariable("http_proxy");
|
||||
var noProxy = Environment.GetEnvironmentVariable("NO_PROXY") ??
|
||||
Environment.GetEnvironmentVariable("no_proxy");
|
||||
if (string.IsNullOrWhiteSpace(httpProxy) && !string.IsNullOrWhiteSpace(noProxy))
|
||||
{
|
||||
score += 0.1;
|
||||
}
|
||||
weights += 0.1;
|
||||
|
||||
var normalizedScore = weights > 0 ? score / weights : 0;
|
||||
var threshold = _options.Value.HeuristicThreshold;
|
||||
|
||||
var isSealed = normalizedScore >= threshold;
|
||||
|
||||
_logger.LogInformation(
|
||||
"Heuristic detection result: score={Score:F2}, threshold={Threshold:F2}, sealed={IsSealed}",
|
||||
normalizedScore,
|
||||
threshold,
|
||||
isSealed);
|
||||
|
||||
return new SealedModeStatus(
|
||||
Sealed: isSealed,
|
||||
Mode: isSealed ? "sealed-heuristic" : "unsealed-heuristic",
|
||||
SealedAt: null,
|
||||
SealedBy: null,
|
||||
BundleVersion: null,
|
||||
BundleDigest: null,
|
||||
LastAdvisoryUpdate: null,
|
||||
AdvisoryStalenessHours: 0,
|
||||
TimeAnchor: null,
|
||||
EgressBlocked: isSealed,
|
||||
NetworkPolicy: isSealed ? "heuristic-detected" : null);
|
||||
}
|
||||
|
||||
private sealed record AirGapStatusDto(
|
||||
[property: JsonPropertyName("sealed")] bool Sealed,
|
||||
[property: JsonPropertyName("sealed_at")] DateTimeOffset? SealedAt,
|
||||
[property: JsonPropertyName("sealed_by")] string? SealedBy,
|
||||
[property: JsonPropertyName("bundle_version")] string? BundleVersion,
|
||||
[property: JsonPropertyName("bundle_digest")] string? BundleDigest,
|
||||
[property: JsonPropertyName("last_advisory_update")] DateTimeOffset? LastAdvisoryUpdate,
|
||||
[property: JsonPropertyName("advisory_staleness_hours")] int AdvisoryStalenessHours,
|
||||
[property: JsonPropertyName("time_anchor")] TimeAnchorDto? TimeAnchor,
|
||||
[property: JsonPropertyName("egress_blocked")] bool EgressBlocked,
|
||||
[property: JsonPropertyName("network_policy")] string? NetworkPolicy);
|
||||
|
||||
private sealed record TimeAnchorDto(
|
||||
[property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp,
|
||||
[property: JsonPropertyName("signature")] string? Signature,
|
||||
[property: JsonPropertyName("valid")] bool Valid,
|
||||
[property: JsonPropertyName("expires_at")] DateTimeOffset? ExpiresAt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the HTTP air-gap status provider.
|
||||
/// </summary>
|
||||
public sealed class AirGapStatusProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base URL of the AirGap controller.
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "http://localhost:8080";
|
||||
|
||||
/// <summary>
|
||||
/// Status endpoint path.
|
||||
/// </summary>
|
||||
public string StatusEndpoint { get; set; } = "/api/v1/airgap/status";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use heuristic fallback when controller is unavailable.
|
||||
/// </summary>
|
||||
public bool UseHeuristicFallback { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Heuristic score threshold (0.0-1.0) to consider environment sealed.
|
||||
/// </summary>
|
||||
public double HeuristicThreshold { get; set; } = 0.7;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the sealed mode marker file.
|
||||
/// </summary>
|
||||
public string? SealedMarkerPath { get; set; } = "/etc/stellaops/sealed";
|
||||
|
||||
/// <summary>
|
||||
/// URL to test external connectivity.
|
||||
/// </summary>
|
||||
public string ConnectivityTestUrl { get; set; } = "https://api.stellaops.org/health";
|
||||
}
|
||||
Reference in New Issue
Block a user