418 lines
14 KiB
Text
418 lines
14 KiB
Text
|
|
# 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 <name> 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 <name> 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 <name> 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)
|
||
|
|
}
|
||
|
|
}
|