#!/usr/bin/env nu # Service Lifecycle Management # Handles starting and stopping services based on deployment mode const SERVICE_PID_DIR = $"($env.HOME)/.provisioning/services/pids" const SERVICE_LOG_DIR = $"($env.HOME)/.provisioning/services/logs" # Start service based on deployment mode export def start-service-by-mode [ service_def: record service_name: string ] -> bool { match $service_def.deployment.mode { "binary" => { start-binary-service $service_def $service_name } "docker" => { start-docker-service $service_def $service_name } "docker-compose" => { start-docker-compose-service $service_def $service_name } "kubernetes" => { start-kubernetes-service $service_def $service_name } "remote" => { # Remote services are not started locally print $"Service '($service_name)' is remote - checking availability..." true } _ => { print $"Unknown deployment mode: ($service_def.deployment.mode)" false } } } # Start binary service def start-binary-service [ service_def: record service_name: string ] -> bool { let binary_config = $service_def.deployment.binary let binary_path = ($binary_config.binary_path | str replace -a '${HOME}' $env.HOME) # Expand binary path let binary_path = if ($binary_path | str starts-with '~') { $binary_path | str replace '~' $env.HOME } else { $binary_path } if not ($binary_path | path exists) { print $"Binary not found: ($binary_path)" return false } let working_dir = if "working_dir" in $binary_config { $binary_config.working_dir | str replace -a '${HOME}' $env.HOME } else { $env.PWD } let log_file = $"($SERVICE_LOG_DIR)/($service_name).log" let pid_file = $"($SERVICE_PID_DIR)/($service_name).pid" # Build command with args let args = $binary_config.args | str join ' ' let env_vars = if "env" in $binary_config { $binary_config.env | transpose key value | each { |row| $"($row.key)=($row.value)" } | str join ' ' } else { "" } # Start in background let cmd = if ($env_vars | is-empty) { $"($binary_path) ($args) >> ($log_file) 2>&1 & echo $!" } else { $"($env_vars) ($binary_path) ($args) >> ($log_file) 2>&1 & echo $!" } try { let pid = (bash -c $cmd | str trim) $pid | save -f $pid_file sleep 1sec # Verify process started let pid_int = ($pid | into int) if (ps | where pid == $pid_int | length) > 0 { print $"✅ Started ($service_name) with PID ($pid)" true } else { print $"❌ Failed to start ($service_name)" false } } catch { print $"❌ Error starting ($service_name)" false } } # Start Docker service def start-docker-service [ service_def: record service_name: string ] -> bool { let docker_config = $service_def.deployment.docker # Check if container already exists let existing = (docker ps -a --filter $"name=($docker_config.container_name)" --format "{{.Names}}" | lines) if ($docker_config.container_name in $existing) { # Container exists, try to start it try { docker start $docker_config.container_name print $"✅ Started existing container: ($docker_config.container_name)" return true } catch { # Remove old container and create new one print $"Removing old container: ($docker_config.container_name)" docker rm -f $docker_config.container_name } } # Build docker run command let mut cmd = ["docker", "run", "-d"] # Container name $cmd = ($cmd | append ["--name", $docker_config.container_name]) # Restart policy let restart_policy = $docker_config.restart_policy? | default "unless-stopped" $cmd = ($cmd | append ["--restart", $restart_policy]) # Ports for port in $docker_config.ports { $cmd = ($cmd | append ["-p", $port]) } # Volumes (expand ${HOME}) for volume in $docker_config.volumes { let expanded_volume = ($volume | str replace -a '${HOME}' $env.HOME) $cmd = ($cmd | append ["-v", $expanded_volume]) } # Environment variables if "environment" in $docker_config { for key in ($docker_config.environment | columns) { let value = ($docker_config.environment | get $key) $cmd = ($cmd | append ["-e", $"($key)=($value)"]) } } # Networks if "networks" in $docker_config { for network in $docker_config.networks { $cmd = ($cmd | append ["--network", $network]) } } # Image $cmd = ($cmd | append $docker_config.image) # Command if "command" in $docker_config { $cmd = ($cmd | append $docker_config.command) } try { let result = (run-external ...$cmd) print $"✅ Started Docker container: ($docker_config.container_name)" true } catch { print $"❌ Failed to start Docker container: ($docker_config.container_name)" false } } # Start Docker Compose service def start-docker-compose-service [ service_def: record service_name: string ] -> bool { let compose_config = $service_def.deployment.docker_compose let compose_file = ($compose_config.compose_file | str replace -a '${HOME}' $env.HOME) if not ($compose_file | path exists) { print $"Compose file not found: ($compose_file)" return false } let project_name = $compose_config.project_name? | default "provisioning" try { if "env_file" in $compose_config { let env_file = ($compose_config.env_file | str replace -a '${HOME}' $env.HOME) docker compose -f $compose_file -p $project_name --env-file $env_file up -d $compose_config.service_name } else { docker compose -f $compose_file -p $project_name up -d $compose_config.service_name } print $"✅ Started Docker Compose service: ($compose_config.service_name)" true } catch { print $"❌ Failed to start Docker Compose service: ($compose_config.service_name)" false } } # Start Kubernetes service def start-kubernetes-service [ service_def: record service_name: string ] -> bool { let k8s_config = $service_def.deployment.kubernetes let kubeconfig = if "kubeconfig" in $k8s_config { ["--kubeconfig", $k8s_config.kubeconfig] } else { [] } # Check if namespace exists try { kubectl ...$kubeconfig get namespace $k8s_config.namespace } catch { print $"Creating namespace: ($k8s_config.namespace)" kubectl ...$kubeconfig create namespace $k8s_config.namespace } # Apply manifests if provided if "manifests_path" in $k8s_config { let manifests_path = ($k8s_config.manifests_path | str replace -a '${HOME}' $env.HOME) if ($manifests_path | path exists) { try { kubectl ...$kubeconfig apply -f $manifests_path -n $k8s_config.namespace print $"✅ Applied manifests for: ($service_name)" } catch { print $"❌ Failed to apply manifests for: ($service_name)" return false } } } # Install Helm chart if provided if "helm_chart" in $k8s_config { let helm_config = $k8s_config.helm_chart let mut helm_cmd = ["helm", "install", $helm_config.release_name, $helm_config.chart] $helm_cmd = ($helm_cmd | append ["-n", $k8s_config.namespace]) if "repo_url" in $helm_config { # Add repo first try { helm repo add $service_name $helm_config.repo_url helm repo update } catch { print $"Warning: Could not add Helm repo" } } if "version" in $helm_config { $helm_cmd = ($helm_cmd | append ["--version", $helm_config.version]) } if "values_file" in $helm_config { let values_file = ($helm_config.values_file | str replace -a '${HOME}' $env.HOME) $helm_cmd = ($helm_cmd | append ["-f", $values_file]) } try { run-external ...$helm_cmd print $"✅ Installed Helm chart: ($helm_config.chart)" true } catch { print $"❌ Failed to install Helm chart: ($helm_config.chart)" false } } else { true } } # Stop service by mode export def stop-service-by-mode [ service_name: string service_def: record force: bool = false ] -> bool { match $service_def.deployment.mode { "binary" => { stop-binary-service $service_name $force } "docker" => { stop-docker-service $service_def $force } "docker-compose" => { stop-docker-compose-service $service_def } "kubernetes" => { stop-kubernetes-service $service_def $force } "remote" => { print $"Service '($service_name)' is remote - cannot stop" true } _ => { print $"Unknown deployment mode: ($service_def.deployment.mode)" false } } } # Stop binary service def stop-binary-service [ service_name: string force: bool ] -> bool { let pid_file = $"($SERVICE_PID_DIR)/($service_name).pid" if not ($pid_file | path exists) { print $"No PID file found for ($service_name)" return true } let pid = (open $pid_file | str trim | into int) if (ps | where pid == $pid | length) == 0 { print $"Process ($pid) not found" rm $pid_file return true } try { if $force { kill -9 $pid } else { kill $pid } sleep 2sec # Check if still running if (ps | where pid == $pid | length) > 0 { print $"Process still running, force killing..." kill -9 $pid sleep 1sec } rm $pid_file print $"✅ Stopped ($service_name)" true } catch { print $"❌ Failed to stop ($service_name)" false } } # Stop Docker service def stop-docker-service [ service_def: record force: bool ] -> bool { let container_name = $service_def.deployment.docker.container_name try { if $force { docker kill $container_name } else { docker stop $container_name } print $"✅ Stopped Docker container: ($container_name)" true } catch { print $"❌ Failed to stop Docker container: ($container_name)" false } } # Stop Docker Compose service def stop-docker-compose-service [ service_def: record ] -> bool { let compose_config = $service_def.deployment.docker_compose let compose_file = ($compose_config.compose_file | str replace -a '${HOME}' $env.HOME) let project_name = $compose_config.project_name? | default "provisioning" try { docker compose -f $compose_file -p $project_name stop $compose_config.service_name print $"✅ Stopped Docker Compose service: ($compose_config.service_name)" true } catch { print $"❌ Failed to stop Docker Compose service: ($compose_config.service_name)" false } } # Stop Kubernetes service def stop-kubernetes-service [ service_def: record force: bool ] -> bool { let k8s_config = $service_def.deployment.kubernetes let kubeconfig = if "kubeconfig" in $k8s_config { ["--kubeconfig", $k8s_config.kubeconfig] } else { [] } # Delete deployment try { if $force { kubectl ...$kubeconfig delete deployment $k8s_config.deployment_name -n $k8s_config.namespace --force --grace-period=0 } else { kubectl ...$kubeconfig delete deployment $k8s_config.deployment_name -n $k8s_config.namespace } print $"✅ Deleted Kubernetes deployment: ($k8s_config.deployment_name)" true } catch { print $"❌ Failed to delete Kubernetes deployment: ($k8s_config.deployment_name)" false } } # Get service PID (for binary services) export def get-service-pid [ service_name: string ] -> int { let pid_file = $"($SERVICE_PID_DIR)/($service_name).pid" if not ($pid_file | path exists) { return 0 } try { open $pid_file | str trim | into int } catch { 0 } } # Kill service process export def kill-service-process [ service_name: string signal: string = "TERM" ] -> bool { let pid = (get-service-pid $service_name) if $pid == 0 { print $"No PID found for ($service_name)" return false } try { kill $"-($signal)" $pid true } catch { print $"Failed to send ($signal) to process ($pid)" false } }