# Health Check Module # Deep health validation for provisioning platform configuration and state use std log # Selective imports (ADR-025 Phase 3 Layer 2). use platform/config/accessor/core.nu [config-get] use platform/user/config.nu [get-user-config-path load-user-config] # Check health of configuration files def check-config-files [] { mut issues = [] let user_config_path = (get-user-config-path) # Check user_config.yaml if not ($user_config_path | path exists) { $issues = ($issues | append "user_config.yaml not found") } # Check active workspace config if user config exists if ($user_config_path | path exists) { let user_config = (load-user-config) let active = ($user_config.active_workspace? | default null) if $active != null { let workspace = ($user_config.workspaces | where name == $active | first) let ws_path = ($workspace.path? | default "") let config_path = ($ws_path | path join "config" | path join "provisioning.yaml") if not ($config_path | path exists) { $issues = ($issues | append $"Workspace config not found: ($config_path)") } } } { check: "Configuration Files" status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" }) issues: $issues recommendation: (if ($issues | is-not-empty) { "Missing config files. Run: provisioning workspace init to create workspace" } else { "All configuration files present" }) } } # Check workspace structure integrity def check-workspace-structure [] { mut issues = [] let user_config = (load-user-config) let active = ($user_config.active_workspace? | default null) if $active == null { $issues = ($issues | append "No active workspace configured") } else { let workspace = ($user_config.workspaces | where name == $active | first) let ws_path = ($workspace.path? | default "") if not ($ws_path | path exists) { $issues = ($issues | append $"Workspace path does not exist: ($ws_path)") } else { # Check required directories let required_dirs = ["infra" "config" "extensions" "runtime"] for dir in $required_dirs { let dir_path = ($ws_path | path join $dir) if not ($dir_path | path exists) { $issues = ($issues | append $"Missing required directory: ($dir)") } } # Check config subdirectories let config_dirs = ["providers" "platform"] for dir in $config_dirs { let dir_path = ($ws_path | path join "config" | path join $dir) if not ($dir_path | path exists) { $issues = ($issues | append $"Missing config directory: config/($dir)") } } } } { check: "Workspace Structure" status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" }) issues: $issues recommendation: (if ($issues | is-not-empty) { "Workspace directories missing. Run: provisioning workspace init to create structure" } else { "Workspace structure complete" }) } } # Check infrastructure state def check-infrastructure-state [] { mut issues = [] mut warnings = [] let user_config = (load-user-config) let active = ($user_config.active_workspace? | default null) if $active != null { let workspace = ($user_config.workspaces | where name == $active | first) let ws_path = ($workspace.path? | default "") let infra_path = ($ws_path | path join "infra") if ($infra_path | path exists) { let infra_dirs = (ls $infra_path | where type == dir | length) if $infra_dirs == 0 { $warnings = ($warnings | append "No infrastructure definitions found") } # Check for orphaned state files let state_path = ($ws_path | path join "runtime" | path join "state") if ($state_path | path exists) { let state_files = (ls $state_path --all | where name =~ r"\.state$" | length) if $state_files > 0 { $warnings = ($warnings | append $"($state_files) state files found - may indicate previous deployments") } } } else { $warnings = ($warnings | append "Infrastructure directory does not exist") } } { check: "Infrastructure State" status: (if ($issues | is-empty) { if ($warnings | is-empty) { "✅ Healthy" } else { "⚠️ Warnings" } } else { "❌ Issues Found" }) issues: ($issues | append $warnings) recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) { "No infrastructure defined. Run: provisioning generate infra --new to create" } else { "Infrastructure configured" }) } } # Check platform services connectivity def check-platform-connectivity [] { mut issues = [] mut warnings = [] # Check orchestrator let orchestrator_url = (config-get "platform.orchestrator.url" "http://localhost:9011") let orchestrator_response = (do -i { http get $"($orchestrator_url)/health" --max-time 2sec }) let orchestrator_healthy = ($orchestrator_response != null) if not $orchestrator_healthy { $warnings = ($warnings | append "Orchestrator not responding - workflows will not be available") } # Check control center let control_center_port = config-get "control_center.port" 8080 let control_center_response = (do -i { http get $"http://localhost:($control_center_port)/health" --max-time 1sec }) let control_center_healthy = ($control_center_response != null) if not $control_center_healthy { $warnings = ($warnings | append "Control Center not responding - web UI will not be available") } # Build recommendation based on what's not running let recommendation = if ($warnings | is-empty) { "All services responding" } else { let not_running = [] let not_running = if not $orchestrator_healthy { $not_running | append "orchestrator" } else { $not_running } let not_running = if not $control_center_healthy { $not_running | append "control-center" } else { $not_running } $"Platform services not running: ($not_running | str join ', '). These services are optional for basic provisioning operations." } { check: "Platform Services" status: (if ($issues | is-empty) { if ($warnings | is-empty) { "✅ Healthy" } else { "⚠️ Warnings" } } else { "❌ Issues Found" }) issues: ($issues | append $warnings) recommendation: $recommendation } } # Check Nickel schemas validity def check-nickel-schemas [] { mut issues = [] mut warnings = [] let nickel_path = config-get "paths.nickel" "provisioning/nickel" if not ($nickel_path | path exists) { $issues = ($issues | append "Nickel directory not found") } else { # Check for main schema files let required_schemas = [ "main.ncl" "settings.ncl" "lib.ncl" "dependencies.ncl" ] for schema in $required_schemas { let schema_path = ($nickel_path | path join $schema) if not ($schema_path | path exists) { $warnings = ($warnings | append $"Schema file not found: ($schema)") } } # Try to compile a simple Nickel file let nickel_bin = (which nickel | get path.0? | default "") if ($nickel_bin | is-not-empty) { do -i { ^nickel fmt --check $nickel_path e> /dev/null o> /dev/null } if ($env.LAST_EXIT_CODE? | default 1) != 0 { $warnings = ($warnings | append "Nickel format check reported issues") } } else { $warnings = ($warnings | append "Nickel CLI not available - cannot validate schemas") } } { check: "Nickel Schemas" status: (if ($issues | is-empty) { if ($warnings | is-empty) { "✅ Healthy" } else { "⚠️ Warnings" } } else { "❌ Issues Found" }) issues: ($issues | append $warnings) recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) { "Nickel schemas missing. Ensure provisioning/schemas/ directory exists" } else { "Schemas validated" }) } } # Check security configuration def check-security-config [] { mut issues = [] mut warnings = [] # Check if encryption is configured let kms_enabled = config-get "kms.enabled" false if not $kms_enabled { $warnings = ($warnings | append "KMS not enabled - secrets will not be encrypted") } # Check authentication let auth_enabled = config-get "auth.enabled" false if not $auth_enabled { $warnings = ($warnings | append "Authentication not enabled - platform access is unprotected") } # Check if SOPS/Age is available let sops_bin = (which sops | get path.0? | default "") let age_bin = (which age | get path.0? | default "") if ($sops_bin | is-empty) { $warnings = ($warnings | append "SOPS not installed - encrypted config editing will not work") } if ($age_bin | is-empty) { $warnings = ($warnings | append "Age not installed - encryption will not work") } { check: "Security Configuration" status: (if ($issues | is-empty) { if ($warnings | is-empty) { "✅ Healthy" } else { "⚠️ Warnings" } } else { "❌ Issues Found" }) issues: ($issues | append $warnings) recommendation: (if ($warnings | is-not-empty) { "Security optional. Install: brew install sops age (encryption tools)" } else { "Security configured" }) } } # Check provider credentials def check-provider-credentials [] { mut issues = [] mut warnings = [] # Check UpCloud credentials let upcloud_user = ($env.UPCLOUD_USERNAME? | default "") let upcloud_pass = ($env.UPCLOUD_PASSWORD? | default "") if ($upcloud_user | is-empty) or ($upcloud_pass | is-empty) { $warnings = ($warnings | append "UpCloud credentials not configured") } # Check AWS credentials let aws_key = ($env.AWS_ACCESS_KEY_ID? | default "") let aws_secret = ($env.AWS_SECRET_ACCESS_KEY? | default "") if ($aws_key | is-empty) or ($aws_secret | is-empty) { $warnings = ($warnings | append "AWS credentials not configured") } { check: "Provider Credentials" status: (if ($issues | is-empty) { if ($warnings | is-empty) { "✅ Healthy" } else { "⚠️ Warnings" } } else { "❌ Issues Found" }) issues: ($issues | append $warnings) recommendation: (if ($warnings | is-not-empty) { "Credentials not set. Export: UPCLOUD_USERNAME/PASSWORD or AWS_ACCESS_KEY_ID/SECRET" } else { "Credentials configured" }) } } # Main health check command # Comprehensive health validation of platform configuration and state export def "provisioning health" [] { print $"(ansi yellow_bold)Provisioning Platform Health Check(ansi reset)\n" mut health_checks = [] # Run all health checks $health_checks = ($health_checks | append (check-config-files)) $health_checks = ($health_checks | append (check-workspace-structure)) $health_checks = ($health_checks | append (check-infrastructure-state)) $health_checks = ($health_checks | append (check-platform-connectivity)) $health_checks = ($health_checks | append (check-nickel-schemas)) $health_checks = ($health_checks | append (check-security-config)) $health_checks = ($health_checks | append (check-provider-credentials)) # Display results print ($health_checks | select check status recommendation) print "" # Show detailed issues let checks_with_issues = ($health_checks | where {|c| ($c.issues | is-not-empty)}) if ($checks_with_issues | is-not-empty) { print $"(ansi red_bold)Issues Found:(ansi reset)\n" for check in $checks_with_issues { print $"(ansi red_bold)($check.check):(ansi reset)" for issue in $check.issues { print $" • ($issue)" } print "" } } else { print $"(ansi green_bold)✅ All health checks passed!(ansi reset)\n" } $health_checks | select check status recommendation } # Get health summary (machine-readable) export def "provisioning health-json" [] { let health_checks = [ (check-config-files) (check-workspace-structure) (check-infrastructure-state) (check-platform-connectivity) (check-nickel-schemas) (check-security-config) (check-provider-credentials) ] let total = ($health_checks | length) let healthy = ($health_checks | where status =~ "✅" | length) let warnings = ($health_checks | where status =~ "⚠️" | length) let failing = ($health_checks | where status =~ "❌" | length) { timestamp: (date now | format date "%Y-%m-%dT%H:%M:%SZ") summary: { total: $total healthy: $healthy warnings: $warnings failing: $failing health_score: (($healthy / $total) * 100 | math round) } checks: $health_checks production_ready: ($failing == 0 and $warnings == 0) } }