provisioning-core/nulib/platform/bootstrap.nu

627 lines
19 KiB
Text
Raw Permalink Normal View History

# 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<record> {
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)
}
}