provisioning-core/nulib/domain/diagnostics/health_check.nu

417 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)
}
}