doctor: complete runtime check documentation sprint
Signed-off-by: master <>
This commit is contained in:
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class ConnectionPoolHealthCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-pool-health.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.pool.health";
|
||||
|
||||
@@ -24,6 +26,9 @@ public sealed class ConnectionPoolHealthCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "pool", "connectivity"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -84,10 +89,10 @@ public sealed class ConnectionPoolHealthCheck : DatabaseCheckBase
|
||||
"Long-running transactions not committed",
|
||||
"Application not properly closing transactions",
|
||||
"Deadlock or lock contention")
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Find idle transactions", "psql -c \"SELECT pid, query FROM pg_stat_activity WHERE state = 'idle in transaction'\"")
|
||||
.AddManualStep(2, "Review application code", "Ensure transactions are properly committed or rolled back")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-pool-health.md"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Find idle transactions", "psql -c \"SELECT pid, query FROM pg_stat_activity WHERE state = 'idle in transaction'\"")
|
||||
.AddManualStep(2, "Review application code", "Ensure transactions are properly committed or rolled back")
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.pool.health")
|
||||
.Build();
|
||||
}
|
||||
@@ -106,10 +111,10 @@ public sealed class ConnectionPoolHealthCheck : DatabaseCheckBase
|
||||
"Connection leak in application",
|
||||
"Too many concurrent requests",
|
||||
"max_connections too low for workload")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review connection pool settings", "Check Npgsql connection string pool size")
|
||||
.AddManualStep(2, "Consider increasing max_connections", "Edit postgresql.conf if appropriate")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-pool-health.md"))
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review connection pool settings", "Check Npgsql connection string pool size")
|
||||
.AddManualStep(2, "Consider increasing max_connections", "Edit postgresql.conf if appropriate")
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.pool.health")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class ConnectionPoolSizeCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-pool-size.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.pool.size";
|
||||
|
||||
@@ -27,6 +29,9 @@ public sealed class ConnectionPoolSizeCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "pool", "configuration"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -67,7 +72,7 @@ public sealed class ConnectionPoolSizeCheck : DatabaseCheckBase
|
||||
"Connection string misconfiguration")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable pooling", "Set Pooling=true in connection string")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-pool-size.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.pool.size")
|
||||
.Build();
|
||||
}
|
||||
@@ -89,7 +94,7 @@ public sealed class ConnectionPoolSizeCheck : DatabaseCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Reduce pool size", $"Set Max Pool Size={availableConnections / 2} in connection string")
|
||||
.AddManualStep(2, "Or increase server limit", "Increase max_connections in postgresql.conf")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-pool-size.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.pool.size")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,11 @@ public abstract class DatabaseCheckBase : IDoctorCheck
|
||||
return !string.IsNullOrEmpty(connectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the runbook URL for the concrete check.
|
||||
/// </summary>
|
||||
protected abstract string RunbookUrl { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
@@ -72,8 +77,9 @@ public abstract class DatabaseCheckBase : IDoctorCheck
|
||||
"Authentication failed",
|
||||
"Network connectivity issue")
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Test connection", "psql -h <host> -U <user> -d <database> -c 'SELECT 1'")
|
||||
.AddManualStep(2, "Check credentials", "Verify database username and password in configuration"))
|
||||
.AddShellStep(1, "Test connection", "psql \"Host=<host>;Port=5432;Database=<database>;Username=<user>;Password=<password>\" -c \"SELECT 1\"")
|
||||
.AddManualStep(2, "Check configuration", "Verify ConnectionStrings__DefaultConnection or Doctor__Plugins__Database__ConnectionString points to the intended PostgreSQL instance")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class DatabaseConnectionCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-connection.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.connection";
|
||||
|
||||
@@ -28,6 +30,9 @@ public sealed class DatabaseConnectionCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class DatabasePermissionsCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-permissions.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.permissions";
|
||||
|
||||
@@ -24,6 +26,9 @@ public sealed class DatabasePermissionsCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "security", "permissions"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -113,7 +118,7 @@ public sealed class DatabasePermissionsCheck : DatabaseCheckBase
|
||||
.AddManualStep(1, "Create dedicated user", "CREATE USER stellaops WITH PASSWORD 'secure_password'")
|
||||
.AddManualStep(2, "Grant minimal permissions", "GRANT CONNECT ON DATABASE stellaops TO stellaops")
|
||||
.AddManualStep(3, "Update connection string", "Change user in connection string to dedicated user")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-permissions.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.permissions")
|
||||
.Build();
|
||||
}
|
||||
@@ -136,7 +141,7 @@ public sealed class DatabasePermissionsCheck : DatabaseCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Grant schema access", $"GRANT USAGE ON SCHEMA public TO {currentUser}")
|
||||
.AddManualStep(2, "Grant table access", $"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO {currentUser}")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-permissions.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.permissions")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class FailedMigrationsCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-migrations-failed.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.migrations.failed";
|
||||
|
||||
@@ -22,6 +24,9 @@ public sealed class FailedMigrationsCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "migrations", "schema"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -89,7 +94,7 @@ public sealed class FailedMigrationsCheck : DatabaseCheckBase
|
||||
.AddManualStep(1, "Review migration logs", "Check application logs for migration error details")
|
||||
.AddManualStep(2, "Fix migration issues", "Resolve the underlying issue and retry migration")
|
||||
.AddShellStep(3, "Retry migrations", "dotnet ef database update")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-migrations-failed.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.migrations.failed")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class PendingMigrationsCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-migrations-pending.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.migrations.pending";
|
||||
|
||||
@@ -27,6 +29,9 @@ public sealed class PendingMigrationsCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "migrations", "schema"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
|
||||
@@ -17,6 +17,7 @@ public sealed class QueryLatencyCheck : DatabaseCheckBase
|
||||
private const int MeasureIterations = 5;
|
||||
private const double WarningThresholdMs = 50;
|
||||
private const double CriticalThresholdMs = 200;
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-latency.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.latency";
|
||||
@@ -33,6 +34,9 @@ public sealed class QueryLatencyCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(3);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -111,7 +115,7 @@ public sealed class QueryLatencyCheck : DatabaseCheckBase
|
||||
.AddShellStep(1, "Check server load", "psql -c \"SELECT * FROM pg_stat_activity WHERE state = 'active'\"")
|
||||
.AddShellStep(2, "Check for locks", "psql -c \"SELECT * FROM pg_locks WHERE NOT granted\"")
|
||||
.AddManualStep(3, "Review network path", "Check network latency between application and database")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-latency.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.latency")
|
||||
.Build();
|
||||
}
|
||||
@@ -131,7 +135,7 @@ public sealed class QueryLatencyCheck : DatabaseCheckBase
|
||||
"Database server moderately loaded")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Monitor trends", "Track latency over time to identify patterns")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-latency.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.latency")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Database.Checks;
|
||||
/// </summary>
|
||||
public sealed class SchemaVersionCheck : DatabaseCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/postgres/db-schema-version.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.db.schema.version";
|
||||
|
||||
@@ -24,6 +26,9 @@ public sealed class SchemaVersionCheck : DatabaseCheckBase
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> Tags => ["database", "schema", "migrations"];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<DoctorCheckResult> ExecuteCheckAsync(
|
||||
DoctorPluginContext context,
|
||||
@@ -95,7 +100,7 @@ public sealed class SchemaVersionCheck : DatabaseCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "List orphaned FKs", "psql -c \"SELECT conname FROM pg_constraint WHERE NOT convalidated\"")
|
||||
.AddManualStep(2, "Review and clean up", "Drop or fix orphaned constraints")
|
||||
.WithRunbookUrl("docs/doctor/articles/postgres/db-schema-version.md"))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification("stella doctor --check check.db.schema.version")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class BackendConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-backend.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.backend";
|
||||
|
||||
@@ -121,12 +123,12 @@ public sealed class BackendConnectivityCheck : IDoctorCheck
|
||||
"Backend service is down",
|
||||
"Backend is returning errors",
|
||||
"Authentication/authorization failure")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check backend logs", "kubectl logs -l app=stellaops-backend")
|
||||
.AddManualStep(2, "Verify backend health", $"curl -v {healthUrl}")
|
||||
.WithRunbookUrl(""))
|
||||
.WithVerification("stella doctor --check check.servicegraph.backend")
|
||||
.Build();
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check backend logs", "docker compose -f devops/compose/docker-compose.stella-ops.yml logs --tail 100 platform-web")
|
||||
.AddManualStep(2, "Verify backend health", $"curl -v {healthUrl}")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.backend")
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) when (ct.IsCancellationRequested)
|
||||
@@ -149,9 +151,9 @@ public sealed class BackendConnectivityCheck : IDoctorCheck
|
||||
"DNS resolution failure",
|
||||
"Firewall blocking connection")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Verify URL", "Check STELLAOPS_BACKEND_URL environment variable")
|
||||
.AddManualStep(1, "Verify URL", "Check StellaOps__BackendUrl or BackendUrl in the deployment configuration")
|
||||
.AddManualStep(2, "Test connectivity", $"curl -v {backendUrl}/health")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.backend")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class CircuitBreakerStatusCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-circuitbreaker.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.circuitbreaker";
|
||||
|
||||
@@ -75,7 +77,7 @@ public sealed class CircuitBreakerStatusCheck : IDoctorCheck
|
||||
.WithCauses("Break duration less than 5 seconds may cause excessive retries")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Increase break duration", "Set Resilience:CircuitBreaker:BreakDurationSeconds to 30")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -87,7 +89,7 @@ public sealed class CircuitBreakerStatusCheck : IDoctorCheck
|
||||
.WithCauses("Threshold of 1 may cause circuit to open on transient failures")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Increase threshold", "Set Resilience:CircuitBreaker:FailureThreshold to 5")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.Build());
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class MessageQueueCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-mq.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.mq";
|
||||
|
||||
@@ -80,13 +82,13 @@ public sealed class MessageQueueCheck : IDoctorCheck
|
||||
"RabbitMQ server is not running",
|
||||
"Network connectivity issues",
|
||||
"Firewall blocking AMQP port")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check RabbitMQ status", "docker ps | grep rabbitmq")
|
||||
.AddManualStep(2, "Check RabbitMQ logs", "docker logs rabbitmq")
|
||||
.AddManualStep(3, "Start RabbitMQ", "docker-compose up -d rabbitmq")
|
||||
.WithRunbookUrl(""))
|
||||
.WithVerification("stella doctor --check check.servicegraph.mq")
|
||||
.Build();
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check RabbitMQ status", "docker compose -f devops/compose/docker-compose.stella-ops.yml ps rabbitmq")
|
||||
.AddManualStep(2, "Check RabbitMQ logs", "docker compose -f devops/compose/docker-compose.stella-ops.yml logs --tail 100 rabbitmq")
|
||||
.AddManualStep(3, "Start RabbitMQ", "docker compose -f devops/compose/docker-compose.stella-ops.yml up -d rabbitmq")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.mq")
|
||||
.Build();
|
||||
}
|
||||
|
||||
await connectTask;
|
||||
@@ -132,9 +134,9 @@ public sealed class MessageQueueCheck : IDoctorCheck
|
||||
"DNS resolution failed",
|
||||
"Network unreachable")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Start RabbitMQ", "docker-compose up -d rabbitmq")
|
||||
.AddManualStep(1, "Start RabbitMQ", "docker compose -f devops/compose/docker-compose.stella-ops.yml up -d rabbitmq")
|
||||
.AddManualStep(2, "Verify DNS", $"nslookup {rabbitHost}")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.mq")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class ServiceEndpointsCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-endpoints.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.endpoints";
|
||||
|
||||
@@ -113,9 +115,9 @@ public sealed class ServiceEndpointsCheck : IDoctorCheck
|
||||
.WithEvidence(evidenceBuilder.Build("Service endpoints"))
|
||||
.WithCauses(failedServices.Select(s => $"{s} service is down or unreachable").ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check service status", "kubectl get pods -l app=stellaops")
|
||||
.AddManualStep(2, "Check service logs", "kubectl logs -l app=stellaops --tail=100")
|
||||
.WithRunbookUrl(""))
|
||||
.AddManualStep(1, "Check service status", "docker compose -f devops/compose/docker-compose.stella-ops.yml ps")
|
||||
.AddManualStep(2, "Check service logs", "docker compose -f devops/compose/docker-compose.stella-ops.yml logs --tail 100 <service-name>")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.endpoints")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class ServiceTimeoutCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-timeouts.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.timeouts";
|
||||
|
||||
@@ -91,7 +93,7 @@ public sealed class ServiceTimeoutCheck : IDoctorCheck
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review timeout values", "Check configuration and adjust timeouts based on expected service latencies")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.timeouts")
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
/// </summary>
|
||||
public sealed class ValkeyConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
private const string RunbookUrl = "docs/doctor/articles/servicegraph/servicegraph-valkey.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.servicegraph.valkey";
|
||||
|
||||
@@ -69,7 +71,7 @@ public sealed class ValkeyConnectivityCheck : IDoctorCheck
|
||||
.WithCauses("Connection string format is invalid")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Fix connection string", "Use format: host:port or host:port,password=xxx")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -95,12 +97,12 @@ public sealed class ValkeyConnectivityCheck : IDoctorCheck
|
||||
"Valkey server is not running",
|
||||
"Network connectivity issues",
|
||||
"Firewall blocking port " + port)
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check Valkey status", "docker ps | grep valkey")
|
||||
.AddManualStep(2, "Test port connectivity", $"nc -zv {host} {port}")
|
||||
.WithRunbookUrl(""))
|
||||
.WithVerification("stella doctor --check check.servicegraph.valkey")
|
||||
.Build();
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check Valkey status", "docker compose -f devops/compose/docker-compose.stella-ops.yml ps valkey")
|
||||
.AddManualStep(2, "Test port connectivity", $"nc -zv {host} {port}")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.valkey")
|
||||
.Build();
|
||||
}
|
||||
|
||||
await connectTask;
|
||||
@@ -149,9 +151,9 @@ public sealed class ValkeyConnectivityCheck : IDoctorCheck
|
||||
"DNS resolution failed",
|
||||
"Network unreachable")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Start Valkey", "docker-compose up -d valkey")
|
||||
.AddManualStep(1, "Start Valkey", "docker compose -f devops/compose/docker-compose.stella-ops.yml up -d valkey")
|
||||
.AddManualStep(2, "Check DNS", $"nslookup {host}")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification("stella doctor --check check.servicegraph.valkey")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
/// </summary>
|
||||
public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-policy-engine.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.verification.policy.engine";
|
||||
|
||||
@@ -26,6 +28,9 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
@@ -75,7 +80,7 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
.Add("FileExists", "false"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Export bundle", "stella verification bundle export --include-policy --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.policy.engine")
|
||||
.Build());
|
||||
}
|
||||
@@ -103,7 +108,7 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
"Policy evaluation not run before export")
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Re-export with policy", "stella verification bundle export --include-policy --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.policy.engine")
|
||||
.Build());
|
||||
}
|
||||
@@ -161,7 +166,7 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable policy engine", "Set Policy:Engine:Enabled to true")
|
||||
.AddManualStep(2, "Configure default policy", "Set Policy:DefaultPolicyRef to a policy reference")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.policy.engine")
|
||||
.Build());
|
||||
}
|
||||
@@ -180,7 +185,7 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure test policy", "Set Doctor:Plugins:Verification:PolicyTest:PolicyRef")
|
||||
.AddManualStep(2, "Or set default", "Set Policy:DefaultPolicyRef for a default policy")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.policy.engine")
|
||||
.Build());
|
||||
}
|
||||
@@ -203,7 +208,7 @@ public sealed class PolicyEngineCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable VEX in policy", "Set Policy:VexAware to true")
|
||||
.AddManualStep(2, "Update policy rules", "Ensure policy considers VEX justifications for vulnerabilities")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.policy.engine")
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
/// </summary>
|
||||
public sealed class SbomValidationCheck : VerificationCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-sbom-validation.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.verification.sbom.validation";
|
||||
|
||||
@@ -28,6 +30,9 @@ public sealed class SbomValidationCheck : VerificationCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
@@ -77,7 +82,7 @@ public sealed class SbomValidationCheck : VerificationCheckBase
|
||||
.Add("FileExists", "false"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Export bundle", "stella verification bundle export --include-sbom --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.sbom.validation")
|
||||
.Build());
|
||||
}
|
||||
@@ -103,7 +108,7 @@ public sealed class SbomValidationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Re-export with SBOM", "stella verification bundle export --include-sbom --output " + bundlePath)
|
||||
.AddManualStep(2, "Generate SBOM", "Enable SBOM generation in your build pipeline")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.sbom.validation")
|
||||
.Build());
|
||||
}
|
||||
@@ -160,7 +165,7 @@ public sealed class SbomValidationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable SBOM generation", "Set Scanner:SbomGeneration:Enabled to true")
|
||||
.AddManualStep(2, "Enable SBOM attestation", "Set Attestor:SbomAttestation:Enabled to true")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.sbom.validation")
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
/// </summary>
|
||||
public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-signature.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.verification.signature";
|
||||
|
||||
@@ -27,6 +29,9 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
@@ -76,7 +81,7 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
.Add("FileExists", "false"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Export bundle", "stella verification bundle export --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.signature")
|
||||
.Build());
|
||||
}
|
||||
@@ -104,7 +109,7 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
.Add("Note", "Bundle should contain DSSE signatures for verification"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Re-export with signatures", "stella verification bundle export --include-signatures --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.signature")
|
||||
.Build());
|
||||
}
|
||||
@@ -157,7 +162,7 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable Sigstore", "Set Sigstore:Enabled to true")
|
||||
.AddManualStep(2, "Configure signing", "Set up signing keys or keyless mode")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -184,7 +189,7 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Test Rekor", $"curl -I {rekorHealthUrl}")
|
||||
.AddManualStep(2, "Or use offline mode", "Configure offline verification bundle")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.signature")
|
||||
.Build();
|
||||
}
|
||||
@@ -213,7 +218,7 @@ public sealed class SignatureVerificationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check network", "Verify connectivity to Rekor")
|
||||
.AddManualStep(2, "Use offline mode", "Configure offline verification bundle")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.signature")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
/// </summary>
|
||||
public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-artifact-pull.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.verification.artifact.pull";
|
||||
|
||||
@@ -27,6 +29,9 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
@@ -79,7 +84,7 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Verify file exists", $"ls -la {bundlePath}")
|
||||
.AddShellStep(2, "Export bundle from online system", "stella verification bundle export --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.artifact.pull")
|
||||
.Build());
|
||||
}
|
||||
@@ -115,7 +120,7 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
.WithCauses("Reference format is incorrect")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Fix reference format", "Use format: oci://registry/repository@sha256:digest or registry/repository@sha256:digest")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.artifact.pull")
|
||||
.Build();
|
||||
}
|
||||
@@ -154,7 +159,7 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
.AddShellStep(1, "Test with crane", $"crane manifest {reference}")
|
||||
.AddManualStep(2, "Check registry credentials", "Ensure registry credentials are configured")
|
||||
.AddManualStep(3, "Verify artifact exists", "Confirm the test artifact has been pushed to the registry")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.artifact.pull")
|
||||
.Build();
|
||||
}
|
||||
@@ -182,7 +187,7 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Update expected digest", $"Set Doctor:Plugins:Verification:TestArtifact:ExpectedDigest to {responseDigest}")
|
||||
.AddManualStep(2, "Or use digest in reference", "Use @sha256:... in the reference instead of :tag")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.artifact.pull")
|
||||
.Build();
|
||||
}
|
||||
@@ -213,7 +218,7 @@ public sealed class TestArtifactPullCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Test registry connectivity", $"curl -I https://{registry}/v2/")
|
||||
.AddManualStep(2, "Check network configuration", "Ensure HTTPS traffic to the registry is allowed")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.artifact.pull")
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ public abstract class VerificationCheckBase : IDoctorCheck
|
||||
/// <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);
|
||||
|
||||
@@ -78,7 +83,8 @@ public abstract class VerificationCheckBase : IDoctorCheck
|
||||
"Authentication failure")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Check network connectivity", "Verify the endpoint is reachable")
|
||||
.AddManualStep(2, "Check credentials", "Verify authentication is configured correctly"))
|
||||
.AddManualStep(2, "Check credentials", "Verify authentication is configured correctly")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
@@ -94,7 +100,8 @@ public abstract class VerificationCheckBase : IDoctorCheck
|
||||
"Network latency is high",
|
||||
"Large artifact size")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Increase timeout", "Set Doctor:Plugins:Verification:HttpTimeoutSeconds to a higher value"))
|
||||
.AddManualStep(1, "Increase timeout", "Set Doctor__Plugins__Verification__HttpTimeoutSeconds to a higher value")
|
||||
.WithRunbookUrl(RunbookUrl))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
@@ -141,7 +148,7 @@ public abstract class VerificationCheckBase : IDoctorCheck
|
||||
/// <summary>
|
||||
/// Gets a skip result for when test artifact is not configured.
|
||||
/// </summary>
|
||||
protected static DoctorCheckResult GetNoTestArtifactConfiguredResult(CheckResultBuilder result, string checkId)
|
||||
protected DoctorCheckResult GetNoTestArtifactConfiguredResult(CheckResultBuilder result, string checkId)
|
||||
{
|
||||
return result
|
||||
.Skip("Test artifact not configured")
|
||||
@@ -150,8 +157,9 @@ public abstract class VerificationCheckBase : IDoctorCheck
|
||||
.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"))
|
||||
.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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
/// </summary>
|
||||
public sealed class VexValidationCheck : VerificationCheckBase
|
||||
{
|
||||
private const string RunbookUrlValue = "docs/doctor/articles/verification/verification-vex-validation.md";
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string CheckId => "check.verification.vex.validation";
|
||||
|
||||
@@ -28,6 +30,9 @@ public sealed class VexValidationCheck : VerificationCheckBase
|
||||
/// <inheritdoc />
|
||||
public override TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string RunbookUrl => RunbookUrlValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
@@ -77,7 +82,7 @@ public sealed class VexValidationCheck : VerificationCheckBase
|
||||
.Add("FileExists", "false"))
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Export bundle", "stella verification bundle export --include-vex --output " + bundlePath)
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.vex.validation")
|
||||
.Build());
|
||||
}
|
||||
@@ -105,7 +110,7 @@ public sealed class VexValidationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddShellStep(1, "Re-export with VEX", "stella verification bundle export --include-vex --output " + bundlePath)
|
||||
.AddManualStep(2, "This may be expected", "VEX documents are only needed when vulnerabilities exist")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.vex.validation")
|
||||
.Build());
|
||||
}
|
||||
@@ -157,7 +162,7 @@ public sealed class VexValidationCheck : VerificationCheckBase
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Enable VEX collection", "Set VexHub:Collection:Enabled to true")
|
||||
.AddManualStep(2, "Configure VEX feeds", "Add vendor VEX feeds to VexHub:Feeds")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -174,7 +179,7 @@ public sealed class VexValidationCheck : VerificationCheckBase
|
||||
.WithCauses("No VEX feed URLs configured")
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Configure VEX feeds", "Add vendor VEX feeds to VexHub:Feeds array")
|
||||
.WithRunbookUrl(""))
|
||||
.WithRunbookUrl(RunbookUrlValue))
|
||||
.WithVerification($"stella doctor --check check.verification.vex.validation")
|
||||
.Build());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Database.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Database.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DatabaseCheckRunbookTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("connection", "docs/doctor/articles/postgres/db-connection.md")]
|
||||
[InlineData("pending", "docs/doctor/articles/postgres/db-migrations-pending.md")]
|
||||
[InlineData("failed", "docs/doctor/articles/postgres/db-migrations-failed.md")]
|
||||
[InlineData("schema", "docs/doctor/articles/postgres/db-schema-version.md")]
|
||||
[InlineData("pool-health", "docs/doctor/articles/postgres/db-pool-health.md")]
|
||||
[InlineData("pool-size", "docs/doctor/articles/postgres/db-pool-size.md")]
|
||||
[InlineData("latency", "docs/doctor/articles/postgres/db-latency.md")]
|
||||
[InlineData("permissions", "docs/doctor/articles/postgres/db-permissions.md")]
|
||||
public async Task RunAsync_WhenConnectionFails_UsesExpectedRunbook(string checkName, string expectedRunbook)
|
||||
{
|
||||
var check = CreateCheck(checkName);
|
||||
var context = CreateContext();
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.NotNull(result.Remediation);
|
||||
Assert.Equal(expectedRunbook, result.Remediation!.RunbookUrl);
|
||||
}
|
||||
|
||||
private static IDoctorCheck CreateCheck(string checkName) => checkName switch
|
||||
{
|
||||
"connection" => new DatabaseConnectionCheck(),
|
||||
"pending" => new PendingMigrationsCheck(),
|
||||
"failed" => new FailedMigrationsCheck(),
|
||||
"schema" => new SchemaVersionCheck(),
|
||||
"pool-health" => new ConnectionPoolHealthCheck(),
|
||||
"pool-size" => new ConnectionPoolSizeCheck(),
|
||||
"latency" => new QueryLatencyCheck(),
|
||||
"permissions" => new DatabasePermissionsCheck(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(checkName), checkName, "Unknown check")
|
||||
};
|
||||
|
||||
private static DoctorPluginContext CreateContext()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:DefaultConnection"] = "Host=127.0.0.1;Port=1;Database=stellaops;Username=stellaops;Password=stellaops;Timeout=1;Command Timeout=1;Pooling=false"
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new EmptyServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:Database")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class EmptyServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.ServiceGraph.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ServiceGraphCheckRunbookTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BackendConnectivityCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new BackendConnectivityCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["StellaOps:BackendUrl"] = "http://127.0.0.1:1"
|
||||
}, includeHttpClientFactory: true);
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-backend.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CircuitBreakerStatusCheck_Warning_UsesRunbook()
|
||||
{
|
||||
var check = new CircuitBreakerStatusCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Resilience:Enabled"] = "true",
|
||||
["Resilience:CircuitBreaker:BreakDurationSeconds"] = "1"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Warn, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-circuitbreaker.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceEndpointsCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new ServiceEndpointsCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["StellaOps:AuthorityUrl"] = "http://127.0.0.1:1"
|
||||
}, includeHttpClientFactory: true);
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-endpoints.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MessageQueueCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new MessageQueueCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["RabbitMQ:Host"] = "127.0.0.1",
|
||||
["RabbitMQ:Port"] = "1"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-mq.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceTimeoutCheck_Warning_UsesRunbook()
|
||||
{
|
||||
var check = new ServiceTimeoutCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["HttpClient:Timeout"] = "301"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Warn, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-timeouts.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValkeyConnectivityCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new ValkeyConnectivityCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Valkey:ConnectionString"] = ":6379"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-valkey.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> values, bool includeHttpClientFactory = false)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(values)
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
if (includeHttpClientFactory)
|
||||
{
|
||||
services.AddHttpClient();
|
||||
}
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = services.BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:ServiceGraph")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Verification\StellaOps.Doctor.Plugins.Verification.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Verification.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VerificationCheckRunbookTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("artifact", "docs/doctor/articles/verification/verification-artifact-pull.md")]
|
||||
[InlineData("signature", "docs/doctor/articles/verification/verification-signature.md")]
|
||||
[InlineData("sbom", "docs/doctor/articles/verification/verification-sbom-validation.md")]
|
||||
[InlineData("vex", "docs/doctor/articles/verification/verification-vex-validation.md")]
|
||||
[InlineData("policy", "docs/doctor/articles/verification/verification-policy-engine.md")]
|
||||
public async Task RunAsync_WhenOfflineBundleMissing_UsesExpectedRunbook(string checkName, string expectedRunbook)
|
||||
{
|
||||
var check = CreateCheck(checkName);
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Doctor:Plugins:Verification:Enabled"] = "true",
|
||||
["Doctor:Plugins:Verification:TestArtifact:OfflineBundlePath"] = Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.json")
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal(expectedRunbook, result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WhenArtifactNotConfigured_UsesBaseRunbook()
|
||||
{
|
||||
var check = new SignatureVerificationCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Doctor:Plugins:Verification:Enabled"] = "true"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Skip, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/verification/verification-signature.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
private static IDoctorCheck CreateCheck(string checkName) => checkName switch
|
||||
{
|
||||
"artifact" => new TestArtifactPullCheck(),
|
||||
"signature" => new SignatureVerificationCheck(),
|
||||
"sbom" => new SbomValidationCheck(),
|
||||
"vex" => new VexValidationCheck(),
|
||||
"policy" => new PolicyEngineCheck(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(checkName), checkName, "Unknown check")
|
||||
};
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> values)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(values)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new EmptyServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:Verification")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class EmptyServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user