626 lines
19 KiB
Text
626 lines
19 KiB
Text
# 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)
|
|
}
|
|
}
|