470 lines
13 KiB
Plaintext
470 lines
13 KiB
Plaintext
|
|
#!/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
|
||
|
|
}
|
||
|
|
}
|