# Platform Bootstrap System # Ensures critical platform services are running before executing provisioning tasks # Infrastructure-agnostic: supports Docker, Kubernetes, remote servers, etc. # Selective imports — absolute paths (ADR-025 Phase 3 Layer 2). # 5 former star-imports reduced to 2 selective imports. The other 3 # (utils/logging.nu, services/lifecycle.nu, services/dependencies.nu) had # zero used symbols in this file — they were dead imports. use platform/config/accessor/core.nu [config-get] use platform/user/config.nu [get-active-workspace get-active-workspace-details get-user-config-dir] use platform/setup/mod.nu [get-config-base-path] use tools/nickel/process.nu [ncl-eval-soft] use platform/services/health.nu [wait-for-service] # Load service deployment configuration def get-service-config [service_name: string] { config-get $"platform.services.($service_name)" { name: $service_name health_check: "http" timeout: 30 description: $"Platform service: ($service_name)" } } # Get deployment configuration from workspace def get-deployment-config [] { # Try to load workspace-specific deployment config let workspace = (get-active-workspace-details) if ($workspace != null) { let workspace_config_path = ($workspace.path | path join "config" "platform" "deployment.toml") if ($workspace_config_path | path exists) { return (open $workspace_config_path) } } # Try to load platform deployment mode configuration (Nickel) let config_base = (get-config-base-path) let deployment_ncl = ($config_base | path join "platform" "deployment-mode.ncl") if ($deployment_ncl | path exists) { let content = (ncl-eval-soft $deployment_ncl [($env.PROVISIONING? | default "/usr/local/provisioning")] null) if $content != null { let deployment_mode = ($content.mode? | default "local") return { deployment: { mode: $deployment_mode location_type: (if $deployment_mode == "local" { "local" } else { "remote" }) } } } } # Final fallback to defaults { deployment: { mode: "local" location_type: "local" } } } # Get deployment mode from configuration def get-deployment-mode [] { let config = (get-deployment-config) $config.deployment.mode? | default "local" } # Get platform services deployment location def get-deployment-location [] { let config = (get-deployment-config) $config.deployment? | default { mode: "local" location_type: "local" } } # Critical services that must be running for provisioning to work. # Only the orchestrator is required for L2+ deployments; control-center # and kms-service are optional platform features. def get-critical-services []: nothing -> list { let orchestrator_endpoint = ( config-get "platform.orchestrator.endpoint" "http://localhost:9011/health" ) [ { name: "orchestrator" health_check: "http" endpoint: $orchestrator_endpoint timeout: 30 description: "Workflow orchestrator" } ] } # Check if a service is healthy def check-service-health [service: record] { match $service.health_check { "http" => { let result = (do { curl -s -f -m 5 $service.endpoint } | complete) $result.exit_code == 0 } "tcp" => { let result = (do { ^nc -zv -w 2 ($service.endpoint | split row ":" | get 0) ($service.endpoint | split row ":" | get 1) } | complete) $result.exit_code == 0 } _ => false } } # Helper to process a single service for bootstrap def process-service-bootstrap [ service: record auto_start: bool verbose: bool timeout: int ] { if $verbose { print $"📋 Checking ($service.name)..." } let is_healthy = (check-service-health $service) if $is_healthy { if $verbose { print $" ✅ ($service.name) is healthy" } { name: $service.name status: "healthy" action: "none" } } else { if $verbose { print $" ⚠️ ($service.name) is not responding" } if $auto_start { if $verbose { print $" 🚀 Starting ($service.name)..." } # Try to start the service let start_result = ( not ((start-platform-service $service.name --verbose=$verbose) == null) ) if $start_result { # Wait for service to be healthy let wait_result = (wait-for-service-health $service --timeout=$timeout --verbose=$verbose) if $wait_result { if $verbose { print $" ✅ ($service.name) started successfully" } { name: $service.name status: "healthy" action: "started" } } else { if $verbose { print $" ❌ ($service.name) failed to become healthy" } { name: $service.name status: "unhealthy" action: "failed_to_start" } } } else { if $verbose { print $" ❌ Failed to start ($service.name)" } { name: $service.name status: "unhealthy" action: "start_failed" } } } else { { name: $service.name status: "unhealthy" action: "not_running" } } } } # Bootstrap platform services export def bootstrap-platform [ --auto-start (-a) # Automatically start services if not running --force (-f) # Force restart services --verbose (-v) # Verbose output --timeout: int = 60 # Timeout in seconds ] { let critical_services = (get-critical-services) if $verbose { print $"🔧 Bootstrapping platform services..." print $" Critical services: ($critical_services | length)" print "" } # Process each service using helper function to avoid closure variable capture let services_status = ($critical_services | each { |service| process-service-bootstrap $service $auto_start $verbose $timeout }) # Check if all services are healthy let all_healthy = ($services_status | all { |s| $s.status == "healthy" }) if $verbose { print "" print "🔍 Bootstrap Summary:" for status in $services_status { let status_icon = if $status.status == "healthy" { "✅" } else { "❌" } print $" ($status_icon) ($status.name): ($status.status) (action: ($status.action))" } print "" } { all_healthy: $all_healthy services: $services_status timestamp: (date now) } } # Start a platform service based on deployment mode def start-platform-service [ service_name: string --verbose (-v) ] { let deployment_location = (get-deployment-location) let deployment_mode = (get-deployment-mode) if $verbose { print $" Deployment mode: ($deployment_mode)" print $" Deployment location: ($deployment_location.location_type)" } # Route to appropriate startup method based on deployment mode match $deployment_mode { "local" => { start-service-local $service_name --verbose=$verbose } "docker-compose" => { start-service-docker-compose $service_name --verbose=$verbose } "kubernetes" => { start-service-kubernetes $service_name --verbose=$verbose } "remote-ssh" => { start-service-remote-ssh $service_name --verbose=$verbose } "systemd" => { start-service-systemd $service_name --verbose=$verbose } _ => { if $verbose { print $" ❌ Unknown deployment mode: ($deployment_mode)" } false } } } # Start service via Docker Compose def start-service-docker-compose [ service_name: string --verbose (-v) ] { let platform_path = (config-get "platform.docker_compose.path" (get-config-base-path | path join "platform")) let compose_file = ($platform_path | path join "docker-compose.yaml") if not ($compose_file | path exists) { if $verbose { print $" ❌ Docker compose file not found: ($compose_file)" } return false } let result = (do { if $verbose { print $" Running: docker-compose -f ($compose_file) up -d ($service_name)" } docker-compose -f $compose_file up -d $service_name } | complete) if $result.exit_code == 0 { true } else { if $verbose { print $" Docker compose error: ($result.stderr)" } false } } # Start service locally via native binary or systemd def start-service-local [ service_name: string --verbose (-v) ] { let os_type = $nu.os-info.name # On Linux, try systemd first if $os_type == "linux" { let systemd_result = (do { if $verbose { print $" Trying systemd: systemctl start ($service_name)" } systemctl start $service_name } | complete) if $systemd_result.exit_code == 0 { return true } } # Fallback (all OS): try binary in ~/.local/bin/ with provisioning- prefix let bin_dir = ($env.HOME | path join ".local" "bin") let local_bin = ($bin_dir | path join $"provisioning-($service_name)") let config_base = (get-config-base-path) let config_dir = ($config_base | path join "platform" "config") if ($local_bin | path exists) { if $verbose { print $" Running binary: ($local_bin)" print $" Config dir: ($config_dir)" } # Derive NICKEL_IMPORT_PATH from config base path automatically # Two cases: # 1. Development: /path/to/project/provisioning/../platform/config/ # → Look for provisioning/ at project root level # 2. User install: ~/.config/provisioning/platform/config/ (Linux) or # ~/Library/Application Support/provisioning/platform/config/ (macOS) # → Use PROVISIONING env var pointing to the development project let nickel_import_path = (do { let normalized_config = ($config_base | path expand) # Go up 2 directories: config -> platform -> project_root let project_root = ($normalized_config | path dirname | path dirname) let provisioning_dir = ($project_root | path join "provisioning") # Case 1: Check if provisioning/ exists at project root level (local development) if ($provisioning_dir | path exists) { if $verbose { print $" NICKEL_IMPORT_PATH (local): ($provisioning_dir)" } $provisioning_dir } else { # Case 2: User install - check if in standard user config location by OS let config_str = ($normalized_config | into string) # Standard OS-aware user config path (same for all callers) let user_config_path = (get-user-config-dir) let is_user_config = ($config_str | str starts-with ($user_config_path | path expand)) if $is_user_config { # For user installs, rely on PROVISIONING env var pointing to the development project if $verbose { print $" User config location detected ($os_type): ($config_str)" print $" Using PROVISIONING env var for schemas" } $env.PROVISIONING? | default "/provisioning" } else { # Fallback for other cases if $verbose { print $" ⚠️ Could not determine provisioning location" } $env.PROVISIONING? | default "/provisioning" } } }) let result = (do { if $verbose { # Show output during verbose mode for debugging with-env { NICKEL_IMPORT_PATH: $nickel_import_path } { ^sh -c $"'($local_bin)' --config-dir '($config_dir)' &" } } else { with-env { NICKEL_IMPORT_PATH: $nickel_import_path } { ^sh -c $"'($local_bin)' --config-dir '($config_dir)' > /dev/null 2>&1 &" } } } | complete) if $result.exit_code == 0 { return true } else { if $verbose { print $" Error starting binary: ($result.stderr)" } return false } } if $verbose { print $" ❌ Could not start ($service_name)" print $" - systemd not available on ($os_type)" print $" - binary not found: ($local_bin)" } false } # Start service via Kubernetes def start-service-kubernetes [ service_name: string --verbose (-v) ] { let kubeconfig = (config-get "platform.kubernetes.kubeconfig" "") let namespace = (config-get "platform.kubernetes.namespace" "default") let manifests_path = (config-get "platform.kubernetes.manifests_path" (get-config-base-path | path join "platform" "k8s")) if $verbose { print $" Kubernetes namespace: ($namespace)" if ($kubeconfig | is-not-empty) { print $" Using kubeconfig: ($kubeconfig)" } } # Try to find manifest for service let manifest_file = ($manifests_path | path join $"($service_name).yaml") if not ($manifest_file | path exists) { if $verbose { print $" ⚠️ Manifest not found: ($manifest_file)" print $" Trying: kubectl rollout restart deployment/($service_name)" } # Try to restart existing deployment let cmd = if ($kubeconfig | is-not-empty) { $"kubectl --kubeconfig=($kubeconfig) -n ($namespace) rollout restart deployment/($service_name)" } else { $"kubectl -n ($namespace) rollout restart deployment/($service_name)" } let result = (do { ^sh -c $cmd } | complete) if $result.exit_code == 0 { return true } else { if $verbose { print $" ❌ Could not restart deployment" } return false } } # Apply manifest if $verbose { print $" Applying manifest: ($manifest_file)" } let cmd = if ($kubeconfig | is-not-empty) { $"kubectl --kubeconfig=($kubeconfig) -n ($namespace) apply -f ($manifest_file)" } else { $"kubectl -n ($namespace) apply -f ($manifest_file)" } let result = (do { ^sh -c $cmd } | complete) if $result.exit_code == 0 { true } else { if $verbose { print $" ❌ Error: could not apply Kubernetes manifest" } false } } # Start service via remote SSH def start-service-remote-ssh [ service_name: string --verbose (-v) ] { let remote_host = (config-get "platform.remote.host" "") let remote_user = (config-get "platform.remote.user" "root") let ssh_key = (config-get "platform.remote.ssh_key" "~/.ssh/id_rsa") let start_command = (config-get $"platform.remote.services.($service_name).start_command" "") if ($remote_host | is-empty) { if $verbose { print $" ❌ Remote host not configured" } return false } if ($start_command | is-empty) { if $verbose { print $" ❌ No start command configured for ($service_name)" } return false } if $verbose { print $" SSH to ($remote_user)@($remote_host): ($start_command)" } let result = (do { ssh -i $ssh_key $"($remote_user)@($remote_host)" $start_command } | complete) if $result.exit_code == 0 { true } else { if $verbose { print $" SSH error: ($result.stderr)" } false } } # Start service via systemd def start-service-systemd [ service_name: string --verbose (-v) ] { if $verbose { print $" Running: systemctl start ($service_name)" } let result = (do { systemctl start $service_name } | complete) if $result.exit_code == 0 { true } else { if $verbose { print $" ❌ Error: could not start service via systemctl" } false } } # Wait for a service to become healthy def wait-for-service-health [ service: record --timeout: int = 60 --verbose (-v) ] { let start_time = (date now) let timeout_duration = ($timeout * 1_000_000_000) # Convert to nanoseconds mut attempts = 0 loop { let is_healthy = (check-service-health $service) $attempts = ($attempts + 1) if $is_healthy { if $verbose { print $" ✅ Service became healthy after ($attempts) attempts" } return true } let elapsed = ((date now) - $start_time) let elapsed_ns = ($elapsed | into int) # Convert duration to nanoseconds if $elapsed_ns > $timeout_duration { if $verbose { let timeout_str = ($timeout | into string) let msg = $"Timeout after ($attempts) attempts ($timeout_str) seconds" print $" ⏱️ ($msg)" } return false } if $verbose and (($attempts mod 3) == 0) { print $" ⏳ Waiting... attempt ($attempts)" } sleep 2sec } false # Fallback - should never reach here due to loop returns } # Get platform service status summary export def platform-status [ --verbose (-v) ] { let critical_services = (get-critical-services) mut status_details = [] for service in $critical_services { let is_healthy = (check-service-health $service) $status_details = ($status_details | append { name: $service.name description: $service.description healthy: $is_healthy endpoint: $service.endpoint }) } let all_healthy = ($status_details | all {|s| $s.healthy}) if $verbose { print "Platform Services Status:" print "==========================" for service in $status_details { let icon = if $service.healthy { "✅" } else { "❌" } print $"($icon) ($service.name)" print $" Description: ($service.description)" print $" Endpoint: ($service.endpoint)" print $" Status: (if $service.healthy { "healthy" } else { "unhealthy" })" print "" } } { all_healthy: $all_healthy services: $status_details timestamp: (date now) } }