#!/usr/bin/env nu # Service Lifecycle Management # Handles starting and stopping services based on deployment mode def get-service-pid-dir []: nothing -> string { $"($env.HOME)/.provisioning/services/pids" } def get-service-log-dir []: nothing -> string { $"($env.HOME)/.provisioning/services/logs" } # Start service based on deployment mode export def start-service-by-mode [ service_def: record service_name: string ]: nothing -> 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 ]: nothing -> 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_dir = (get-service-log-dir) let log_file = $"($log_dir)/($service_name).log" let pid_dir = (get-service-pid-dir) let pid_file = $"($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 $!" } let result = (do { bash -c $cmd | str trim } | complete) if $result.exit_code == 0 { let pid = $result.stdout $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 } } else { print $"❌ Error starting [$service_name]: ($result.stderr)" false } } # Start Docker service def start-docker-service [ service_def: record service_name: string ]: nothing -> 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 let result = (do { docker start $docker_config.container_name } | complete) if $result.exit_code == 0 { print $"✅ Started existing container: ($docker_config.container_name)" return true } else { # 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 by collecting all components let restart_policy = $docker_config.restart_policy? | default "unless-stopped" let port_args = ( $docker_config.ports | flat-map {|port| ["-p", $port]} ) let volume_args = ( $docker_config.volumes | each {|volume| let expanded = ($volume | str replace -a '${HOME}' $env.HOME) ["-v", $expanded] } | flatten ) let env_args = if "environment" in $docker_config { ($docker_config.environment | columns) | each {|key| let value = ($docker_config.environment | get $key) ["-e", $"($key)=($value)"] } | flatten } else { [] } let network_args = if "networks" in $docker_config { $docker_config.networks | flat-map {|network| ["--network", $network]} } else { [] } let command_args = if "command" in $docker_config { $docker_config.command } else { [] } let cmd = [ "docker", "run", "-d", "--name", $docker_config.container_name, "--restart", $restart_policy ] | append $port_args | append $volume_args | append $env_args | append $network_args | append $docker_config.image | if ($command_args | is-not-empty) { append $command_args } else { . } let result = (do { run-external ...$cmd } | complete) if $result.exit_code == 0 { print $"✅ Started Docker container: ($docker_config.container_name)" true } else { print $"❌ Failed to start Docker container: ($docker_config.container_name) - ($result.stderr)" false } } # Start Docker Compose service def start-docker-compose-service [ service_def: record service_name: string ]: nothing -> 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" let result = if "env_file" in $compose_config { let env_file = ($compose_config.env_file | str replace -a '${HOME}' $env.HOME) (do { docker compose -f $compose_file -p $project_name --env-file $env_file up -d $compose_config.service_name } | complete) } else { (do { docker compose -f $compose_file -p $project_name up -d $compose_config.service_name } | complete) } if $result.exit_code == 0 { print $"✅ Started Docker Compose service: ($compose_config.service_name)" true } else { print $"❌ Failed to start Docker Compose service: ($compose_config.service_name) - ($result.stderr)" false } } # Start Kubernetes service def start-kubernetes-service [ service_def: record service_name: string ]: nothing -> bool { let k8s_config = $service_def.deployment.kubernetes let kubeconfig = if "kubeconfig" in $k8s_config { ["--kubeconfig", $k8s_config.kubeconfig] } else { [] } # Check if namespace exists let ns_check = (do { kubectl ...$kubeconfig get namespace $k8s_config.namespace } | complete) if $ns_check.exit_code != 0 { 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) { let result = (do { kubectl ...$kubeconfig apply -f $manifests_path -n $k8s_config.namespace } | complete) if $result.exit_code == 0 { print $"✅ Applied manifests for: [$service_name]" } else { print $"❌ Failed to apply manifests for: [$service_name] - ($result.stderr)" return false } } } # Install Helm chart if provided if "helm_chart" in $k8s_config { let helm_config = $k8s_config.helm_chart let base_helm_cmd = ["helm", "install", $helm_config.release_name, $helm_config.chart, "-n", $k8s_config.namespace] if "repo_url" in $helm_config { # Add repo first let repo_result = (do { helm repo add $service_name $helm_config.repo_url helm repo update } | complete) if $repo_result.exit_code != 0 { print $"Warning: Could not add Helm repo - ($repo_result.stderr)" } } let with_version = if "version" in $helm_config { $base_helm_cmd | append ["--version", $helm_config.version] } else { $base_helm_cmd } let helm_cmd = if "values_file" in $helm_config { let values_file = ($helm_config.values_file | str replace -a '${HOME}' $env.HOME) $with_version | append ["-f", $values_file] } else { $with_version } let result = (do { run-external ...$helm_cmd } | complete) if $result.exit_code == 0 { print $"✅ Installed Helm chart: ($helm_config.chart)" true } else { print $"❌ Failed to install Helm chart: ($helm_config.chart) - ($result.stderr)" false } } else { true } } # Stop service by mode export def stop-service-by-mode [ service_name: string service_def: record force: bool = false ]: nothing -> 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 ]: nothing -> bool { let pid_dir = (get-service-pid-dir) let pid_file = $"($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 } let result = (do { if $force { kill -9 $pid } else { kill $pid } } | complete) if $result.exit_code == 0 { 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 } else { print $"❌ Failed to stop [$service_name]: ($result.stderr)" false } } # Stop Docker service def stop-docker-service [ service_def: record force: bool ]: nothing -> bool { let container_name = $service_def.deployment.docker.container_name let result = (do { if $force { docker kill $container_name } else { docker stop $container_name } } | complete) if $result.exit_code == 0 { print $"✅ Stopped Docker container: [$container_name]" true } else { print $"❌ Failed to stop Docker container: [$container_name] - ($result.stderr)" false } } # Stop Docker Compose service def stop-docker-compose-service [ service_def: record ]: nothing -> 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" let result = (do { docker compose -f $compose_file -p $project_name stop $compose_config.service_name } | complete) if $result.exit_code == 0 { print $"✅ Stopped Docker Compose service: ($compose_config.service_name)" true } else { print $"❌ Failed to stop Docker Compose service: ($compose_config.service_name) - ($result.stderr)" false } } # Stop Kubernetes service def stop-kubernetes-service [ service_def: record force: bool ]: nothing -> bool { let k8s_config = $service_def.deployment.kubernetes let kubeconfig = if "kubeconfig" in $k8s_config { ["--kubeconfig", $k8s_config.kubeconfig] } else { [] } # Delete deployment let result = (do { 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 } } | complete) if $result.exit_code == 0 { print $"✅ Deleted Kubernetes deployment: ($k8s_config.deployment_name)" true } else { print $"❌ Failed to delete Kubernetes deployment: ($k8s_config.deployment_name) - ($result.stderr)" false } } # Get service PID (for binary services) export 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 } } # Kill service process export def kill-service-process [ service_name: string signal: string = "TERM" ]: nothing -> bool { let pid = (get-service-pid $service_name) if $pid == 0 { print $"No PID found for [$service_name]" return false } let result = (do { bash -c $"kill -($signal) ($pid)" } | complete) if $result.exit_code == 0 { true } else { print $"Failed to send [$signal] to process [$pid]: ($result.stderr)" false } }