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; /// /// Base class for verification checks providing common functionality. /// public abstract class VerificationCheckBase : IDoctorCheck { /// /// Plugin identifier for verification checks. /// protected const string PluginId = "stellaops.doctor.verification"; /// /// Category name for verification checks. /// protected const string CategoryName = "Security"; /// public abstract string CheckId { get; } /// public abstract string Name { get; } /// public abstract string Description { get; } /// public virtual DoctorSeverity DefaultSeverity => DoctorSeverity.Fail; /// public abstract IReadOnlyList Tags { get; } /// /// Gets the runbook URL for the concrete check. /// protected abstract string RunbookUrl { get; } /// public virtual TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10); /// public virtual bool CanRun(DoctorPluginContext context) { var options = VerificationPlugin.GetOptions(context); return options.Enabled; } /// public async Task 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(); } } /// /// Executes the specific check logic. /// protected abstract Task ExecuteCheckAsync( DoctorPluginContext context, VerificationPluginOptions options, CheckResultBuilder result, CancellationToken ct); /// /// Creates an HttpClient with configured timeout. /// protected static HttpClient CreateHttpClient(VerificationPluginOptions options) { return new HttpClient { Timeout = TimeSpan.FromSeconds(options.HttpTimeoutSeconds) }; } /// /// Checks if a test artifact is configured. /// protected static bool HasTestArtifactConfigured(VerificationPluginOptions options) { return !string.IsNullOrEmpty(options.TestArtifact.Reference) || !string.IsNullOrEmpty(options.TestArtifact.OfflineBundlePath); } /// /// Gets a skip result for when test artifact is not configured. /// 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(); } }