// Package validation provides a framework for validating cloud provider // configurations and API credentials. It defines structured check results, // a Check interface that providers implement, and a Runner that orchestrates // checks and produces reports. // // Usage: // // runner := validation.NewRunner(provider) // report := runner.Run(ctx) // fmt.Println(report.Format()) package validation import ( "context" "fmt" "strings" "time" ) // Status represents the outcome of a single validation check. type Status string const ( // Pass means the check succeeded. Pass Status = "PASS" // Fail means the check failed — configuration is invalid or credentials are bad. Fail Status = "FAIL" // Warn means the check passed but with a non-blocking issue (e.g. quota near limit). Warn Status = "WARN" // Skip means the check was not applicable (e.g. no token provided for a secondary provider). Skip Status = "SKIP" // Error means the check could not complete due to an unexpected error (network, etc.). Error Status = "ERROR" ) // CheckCategory groups related checks for organized output. type CheckCategory string const ( CategoryCredentials CheckCategory = "Credentials" CategoryConnectivity CheckCategory = "Connectivity" CategorySSH CheckCategory = "SSH Keys" CategoryServer CheckCategory = "Server Config" CategoryQuota CheckCategory = "Quotas" CategoryAccount CheckCategory = "Account" ) // CheckResult is the outcome of a single validation check. type CheckResult struct { // Name is a short identifier for the check (e.g. "token-auth", "ssh-keys"). Name string // Category groups this check with related checks. Category CheckCategory // Status is the outcome. Status Status // Message is a human-readable description of the result. Message string // Detail is optional extra info (e.g. SSH key fingerprints found, quota numbers). Detail string // Duration tracks how long the check took (useful for API call timing). Duration time.Duration } // Passed returns true if the status is Pass or Warn. func (r CheckResult) Passed() bool { return r.Status == Pass || r.Status == Warn } // Icon returns a terminal-friendly icon for the status. func (r CheckResult) Icon() string { switch r.Status { case Pass: return "✓" case Fail: return "✗" case Warn: return "!" case Skip: return "—" case Error: return "⚠" default: return "?" } } // Check is a single validation step. Provider implementations register checks // via their Checks() method. Each check should be independent — checks run // concurrently and the failure of one should not affect others. type Check interface { // Name returns a short kebab-case identifier (e.g. "token-format"). Name() string // Category returns the check's grouping category. Category() CheckCategory // Run executes the check and returns the result. Run(ctx context.Context) CheckResult } // CheckFunc is a convenience type for creating checks from functions. type CheckFunc struct { CategoryField CheckCategory NameField string RunFunc func(ctx context.Context) CheckResult } func (c CheckFunc) Name() string { return c.NameField } func (c CheckFunc) Category() CheckCategory { return c.CategoryField } func (c CheckFunc) Run(ctx context.Context) CheckResult { return c.RunFunc(ctx) } // Report is the aggregate result of all validation checks for a provider. type Report struct { // Provider is the name of the cloud provider that was validated. Provider string // Results contains the outcome of each check, in the order they were run. Results []CheckResult // TotalDuration is the wall-clock time for all checks combined. TotalDuration time.Duration } // Summary returns pass/fail/warn/skip/error counts. func (r *Report) Summary() map[Status]int { counts := map[Status]int{ Pass: 0, Fail: 0, Warn: 0, Skip: 0, Error: 0, } for _, res := range r.Results { counts[res.Status]++ } return counts } // HasFailures returns true if any check failed or errored. func (r *Report) HasFailures() bool { for _, res := range r.Results { if res.Status == Fail || res.Status == Error { return true } } return false } // AllPassed returns true if every check passed or was skipped. func (r *Report) AllPassed() bool { return !r.HasFailures() } // ByCategory returns results grouped by category, preserving order within each group. func (r *Report) ByCategory() map[CheckCategory][]CheckResult { out := make(map[CheckCategory][]CheckResult) for _, res := range r.Results { out[res.Category] = append(out[res.Category], res) } return out } // Format produces a human-readable terminal output of the report. func (r *Report) Format() string { var b strings.Builder summary := r.Summary() fmt.Fprintf(&b, "\n Provider Validation: %s\n", r.Provider) fmt.Fprintf(&b, " %s\n", strings.Repeat("─", 50)) // Group by category categories := []CheckCategory{ CategoryCredentials, CategoryConnectivity, CategorySSH, CategoryServer, CategoryQuota, CategoryAccount, } seen := make(map[CheckCategory]bool) for _, cat := range categories { results := r.ByCategory()[cat] if len(results) == 0 { continue } seen[cat] = true fmt.Fprintf(&b, "\n [%s]\n", cat) for _, res := range results { fmt.Fprintf(&b, " %s %-25s %s\n", res.Icon(), res.Name, res.Message) if res.Detail != "" { fmt.Fprintf(&b, " %s\n", res.Detail) } } } // Print any results in uncategorized groups for _, res := range r.Results { if !seen[res.Category] && len(r.ByCategory()[res.Category]) > 0 { fmt.Fprintf(&b, "\n [%s]\n", res.Category) for _, r2 := range r.ByCategory()[res.Category] { fmt.Fprintf(&b, " %s %-25s %s\n", r2.Icon(), r2.Name, r2.Message) if r2.Detail != "" { fmt.Fprintf(&b, " %s\n", r2.Detail) } } seen[res.Category] = true } } // Summary line fmt.Fprintf(&b, "\n %s\n", strings.Repeat("─", 50)) fmt.Fprintf(&b, " Total: %d checks in %s", len(r.Results), r.TotalDuration.Round(time.Millisecond)) fmt.Fprintf(&b, " | ✓%d ✗%d !%d —%d ⚠%d\n", summary[Pass], summary[Fail], summary[Warn], summary[Skip], summary[Error]) if r.HasFailures() { fmt.Fprintf(&b, "\n Result: FAIL — fix the issues above before deploying\n") } else { fmt.Fprintf(&b, "\n Result: OK — ready to deploy\n") } return b.String() } // Validatable is the interface that cloud provider implementations must satisfy // to participate in the validation framework. It extends the basic Provider // interface with structured validation support. type Validatable interface { // ProviderName returns the display name of the provider (e.g. "Hetzner Cloud"). ProviderName() string // Checks returns all validation checks for this provider. // The provider should inspect its own config to determine which checks // are applicable (e.g. skip SSH key checks if no token is set). Checks(ctx context.Context) []Check } // Runner executes all validation checks for a provider and produces a Report. type Runner struct { provider Validatable timeout time.Duration } // NewRunner creates a validation runner for the given provider. func NewRunner(provider Validatable) *Runner { return &Runner{ provider: provider, timeout: 30 * time.Second, } } // SetTimeout configures the per-check timeout. Default is 30s. func (r *Runner) SetTimeout(d time.Duration) { r.timeout = d } // Run executes all checks and returns the aggregate report. // Checks run sequentially for predictable output ordering. // Each check respects the configured timeout. func (r *Runner) Run(ctx context.Context) *Report { start := time.Now() // Collect checks checks := r.provider.Checks(ctx) report := &Report{ Provider: r.provider.ProviderName(), Results: make([]CheckResult, 0, len(checks)), } for _, check := range checks { // Per-check timeout checkCtx, cancel := context.WithTimeout(ctx, r.timeout) checkStart := time.Now() result := check.Run(checkCtx) result.Duration = time.Since(checkStart) // Ensure the result has its name/category set from the check if result.Name == "" { result.Name = check.Name() } if result.Category == "" { result.Category = check.Category() } cancel() report.Results = append(report.Results, result) } report.TotalDuration = time.Since(start) return report }