470 lines
13 KiB
Plaintext
Raw Permalink Normal View History

2025-10-07 10:32:04 +01:00
#!/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
}
}