#!/usr/bin/env nu # Service Manager Core # Manages platform service lifecycle, registry, and health checks use ../config/loader.nu * def get-service-state-dir []: nothing -> string { $"($env.HOME)/.provisioning/services/state" } def get-service-pid-dir []: nothing -> string { $"($env.HOME)/.provisioning/services/pids" } def get-service-log-dir []: nothing -> string { $"($env.HOME)/.provisioning/services/logs" } # Load service registry from configuration export def load-service-registry []: nothing -> record { let config = (load-provisioning-config) # Load services from config file let services_config = $"($config.paths.base)/config/services.toml" if not ($services_config | path exists) { error make { msg: "Service registry not found" label: { text: $"Missing: ($services_config)" span: (metadata $services_config).span } } } open $services_config | get services } # Get service definition by name export def get-service-definition [ service_name: string ]: nothing -> record { let registry = (load-service-registry) if $service_name not-in ($registry | columns) { let available = (($registry | columns) | str join ", ") error make { msg: $"Service '($service_name)' not found in registry" label: { text: $"Available services: ($available)" span: (metadata $service_name).span } } } $registry | get $service_name } # Check if service is running export def is-service-running [ service_name: string ]: nothing -> bool { let service_def = (get-service-definition $service_name) match $service_def.deployment.mode { "binary" => { let pid_dir = (get-service-pid-dir) let pid_file = $"($pid_dir)/($service_name).pid" if not ($pid_file | path exists) { return false } let pid = (open $pid_file | str trim | into int) (ps | where pid == $pid | length) > 0 } "docker" => { let container_name = $service_def.deployment.docker.container_name let containers = (docker ps --filter $"name=($container_name)" --format "{{.Names}}" | lines) $container_name in $containers } "docker-compose" => { let compose_service = $service_def.deployment.docker_compose.service_name let project = $service_def.deployment.docker_compose.project_name? | default "provisioning" let containers = (docker ps --filter $"label=com.docker.compose.project=($project)" --filter $"label=com.docker.compose.service=($compose_service)" --format "{{.Names}}" | lines) ($containers | length) > 0 } "kubernetes" => { # Check if deployment exists and has ready pods let namespace = $service_def.deployment.kubernetes.namespace let deployment = $service_def.deployment.kubernetes.deployment_name let result = (do { kubectl get deployment $deployment -n $namespace -o json | from json | get status.readyReplicas? | default 0 } | complete) if $result.exit_code == 0 { ($result.stdout | into int) > 0 } else { false } } "remote" => { # For remote services, check health endpoint check-service-health $service_name | get healthy } _ => { false } } } # Get service status export def get-service-status [ service_name: string ]: nothing -> record { let is_running = (is-service-running $service_name) let service_def = (get-service-definition $service_name) let state_dir = (get-service-state-dir) let state_file = $"($state_dir)/($service_name).json" let state = if ($state_file | path exists) { open $state_file } else { { name: $service_name status: "unknown" health_status: "unknown" restart_count: 0 } } { name: $service_name type: $service_def.type category: $service_def.category status: (if $is_running { "running" } else { "stopped" }) deployment_mode: $service_def.deployment.mode pid: (get-service-pid $service_name) uptime: (get-service-uptime $service_name) health_status: $state.health_status restart_count: $state.restart_count auto_start: $service_def.startup.auto_start dependencies: $service_def.dependencies } } # Get service PID def get-service-pid [ service_name: string ]: nothing -> int { let pid_dir = (get-service-pid-dir) let pid_file = $"($pid_dir)/[$service_name].pid" if not ($pid_file | path exists) { return 0 } let result = (do { open $pid_file | str trim | into int } | complete) if $result.exit_code == 0 { $result.stdout | into int } else { 0 } } # Get service uptime in seconds def get-service-uptime [ service_name: string ]: nothing -> int { let state_dir = (get-service-state-dir) let state_file = $"($state_dir)/[$service_name].json" if not ($state_file | path exists) { return 0 } let state = (open $state_file) if "started_at" not-in $state { return 0 } let result = (do { let started = ($state.started_at | into datetime) let now = (date now) (($now - $started) | into int) / 1_000_000_000 # Convert nanoseconds to seconds } | complete) if $result.exit_code == 0 { $result.stdout | into int } else { 0 } } # Start service export def start-service [ service_name: string --force (-f) ]: nothing -> bool { # Ensure state directories exist mkdir (get-service-state-dir) mkdir (get-service-pid-dir) mkdir (get-service-log-dir) if (is-service-running $service_name) and not $force { print $"Service '($service_name)' is already running" return true } if $force and (is-service-running $service_name) { print $"Stopping existing instance of '($service_name)'..." stop-service $service_name --force sleep 2sec } let service_def = (get-service-definition $service_name) # Check dependencies for dep in $service_def.dependencies { if not (is-service-running $dep) { print $"Starting dependency: ($dep)" start-service $dep } } print $"Starting service: ($service_name)" # Delegate to lifecycle module use ./lifecycle.nu start-service-by-mode let result = (start-service-by-mode $service_def $service_name) if $result { # Update state let state = { name: $service_name status: "running" started_at: (date now | format date "%Y-%m-%dT%H:%M:%S%z") health_status: "unknown" restart_count: 0 } let state_dir = (get-service-state-dir) $state | to json | save -f $"($state_dir)/($service_name).json" # Wait for health check print $"Waiting for ($service_name) to become healthy..." use ./health.nu wait-for-service wait-for-service $service_name $service_def.startup.start_timeout } else { print $"Failed to start service: ($service_name)" false } } # Stop service export def stop-service [ service_name: string --force (-f) ]: nothing -> bool { if not (is-service-running $service_name) { print $"Service '($service_name)' is not running" return true } print $"Stopping service: ($service_name)" let service_def = (get-service-definition $service_name) # Delegate to lifecycle module use ./lifecycle.nu stop-service-by-mode let result = (stop-service-by-mode $service_name $service_def $force) if $result { # Update state let state_dir = (get-service-state-dir) let state_file = $"($state_dir)/($service_name).json" if ($state_file | path exists) { let state = (open $state_file) { ...$state status: "stopped" stopped_at: (date now | format date "%Y-%m-%dT%H:%M:%S%z") } | to json | save -f $state_file } # Remove PID file let pid_dir = (get-service-pid-dir) let pid_file = $"($pid_dir)/($service_name).pid" if ($pid_file | path exists) { rm $pid_file } } $result } # Restart service export def restart-service [ service_name: string ]: nothing -> bool { print $"Restarting service: ($service_name)" if (is-service-running $service_name) { stop-service $service_name --force sleep 2sec } start-service $service_name } # Check service health export def check-service-health [ service_name: string ]: nothing -> record { let service_def = (get-service-definition $service_name) use ./health.nu perform-health-check perform-health-check $service_name $service_def.health_check } # Wait for service to be healthy export def wait-for-service-health [ service_name: string timeout: int = 60 ]: nothing -> bool { use ./health.nu wait-for-service wait-for-service $service_name $timeout } # Get all services export def list-all-services []: nothing -> list { let registry = (load-service-registry) $registry | columns | each { |name| get-service-status $name } } # Get running services export def list-running-services []: nothing -> list { list-all-services | where status == "running" } # Get service logs export def get-service-logs [ service_name: string --lines: int = 50 --follow (-f) ]: nothing -> string { let log_dir = (get-service-log-dir) let log_file = $"($log_dir)/($service_name).log" if not ($log_file | path exists) { return $"No logs found for ($service_name)" } if $follow { tail -f -n $lines $log_file } else { tail -n $lines $log_file } } # Initialize service state directories export def init-service-state []: nothing -> nothing { mkdir (get-service-state-dir) mkdir (get-service-pid-dir) mkdir (get-service-log-dir) }