Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Verification/Checks/VerificationCheckBase.cs
2026-03-31 23:26:24 +03:00

166 lines
6.0 KiB
C#

using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
using StellaOps.Doctor.Plugins.Verification.Configuration;
namespace StellaOps.Doctor.Plugins.Verification.Checks;
/// <summary>
/// Base class for verification checks providing common functionality.
/// </summary>
public abstract class VerificationCheckBase : IDoctorCheck
{
/// <summary>
/// Plugin identifier for verification checks.
/// </summary>
protected const string PluginId = "stellaops.doctor.verification";
/// <summary>
/// Category name for verification checks.
/// </summary>
protected const string CategoryName = "Security";
/// <inheritdoc />
public abstract string CheckId { get; }
/// <inheritdoc />
public abstract string Name { get; }
/// <inheritdoc />
public abstract string Description { get; }
/// <inheritdoc />
public virtual DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
/// <inheritdoc />
public abstract IReadOnlyList<string> Tags { get; }
/// <summary>
/// Gets the runbook URL for the concrete check.
/// </summary>
protected abstract string RunbookUrl { get; }
/// <inheritdoc />
public virtual TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
/// <inheritdoc />
public virtual bool CanRun(DoctorPluginContext context)
{
var options = VerificationPlugin.GetOptions(context);
return options.Enabled;
}
/// <inheritdoc />
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, PluginId, CategoryName);
var options = VerificationPlugin.GetOptions(context);
if (!options.Enabled)
{
return result
.Skip("Verification plugin is disabled")
.WithEvidence("Configuration", e => e
.Add("Enabled", "false"))
.Build();
}
try
{
return await ExecuteCheckAsync(context, options, result, ct);
}
catch (HttpRequestException ex)
{
return result
.Fail($"Network error: {ex.Message}")
.WithEvidence("Error details", e => e
.Add("ExceptionType", ex.GetType().Name)
.Add("Message", ex.Message)
.Add("StatusCode", ex.StatusCode?.ToString() ?? "(none)"))
.WithCauses(
"Network connectivity issue",
"Registry or endpoint unreachable",
"Authentication failure")
.WithRemediation(r => r
.AddManualStep(1, "Check network connectivity", "Verify the endpoint is reachable")
.AddManualStep(2, "Check credentials", "Verify authentication is configured correctly")
.WithRunbookUrl(RunbookUrl))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (TaskCanceledException ex) when (ex.CancellationToken != ct)
{
return result
.Fail("Request timed out")
.WithEvidence("Error details", e => e
.Add("ExceptionType", "TimeoutException")
.Add("Message", "The request timed out before completing"))
.WithCauses(
"Endpoint is slow to respond",
"Network latency is high",
"Large artifact size")
.WithRemediation(r => r
.AddManualStep(1, "Increase timeout", "Set Doctor__Plugins__Verification__HttpTimeoutSeconds to a higher value")
.WithRunbookUrl(RunbookUrl))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
catch (Exception ex)
{
return result
.Fail($"Unexpected error: {ex.Message}")
.WithEvidence("Error details", e => e
.Add("ExceptionType", ex.GetType().Name)
.Add("Message", ex.Message))
.Build();
}
}
/// <summary>
/// Executes the specific check logic.
/// </summary>
protected abstract Task<DoctorCheckResult> ExecuteCheckAsync(
DoctorPluginContext context,
VerificationPluginOptions options,
CheckResultBuilder result,
CancellationToken ct);
/// <summary>
/// Creates an HttpClient with configured timeout.
/// </summary>
protected static HttpClient CreateHttpClient(VerificationPluginOptions options)
{
return new HttpClient
{
Timeout = TimeSpan.FromSeconds(options.HttpTimeoutSeconds)
};
}
/// <summary>
/// Checks if a test artifact is configured.
/// </summary>
protected static bool HasTestArtifactConfigured(VerificationPluginOptions options)
{
return !string.IsNullOrEmpty(options.TestArtifact.Reference)
|| !string.IsNullOrEmpty(options.TestArtifact.OfflineBundlePath);
}
/// <summary>
/// Gets a skip result for when test artifact is not configured.
/// </summary>
protected DoctorCheckResult GetNoTestArtifactConfiguredResult(CheckResultBuilder result, string checkId)
{
return result
.Skip("Test artifact not configured")
.WithEvidence("Configuration", e => e
.Add("TestArtifactReference", "(not set)")
.Add("OfflineBundlePath", "(not set)")
.Add("Note", "Configure a test artifact to enable verification pipeline checks"))
.WithRemediation(r => r
.AddManualStep(1, "Configure test artifact", "Set Doctor__Plugins__Verification__TestArtifact__Reference to an OCI reference")
.AddManualStep(2, "Or use offline bundle", "Set Doctor__Plugins__Verification__TestArtifact__OfflineBundlePath for air-gap environments")
.WithRunbookUrl(RunbookUrl))
.Build();
}
}