Jesús Pérez 85ce530733
feat: update provisioning core CLI, libraries, and plugins
Update core components including CLI, Nushell libraries, plugins system,
and utility scripts for the provisioning system.

CLI Updates:
- Command implementations
- CLI utilities and dispatching
- Help system improvements
- Command validation

Library Updates:
- Configuration management system
- Infrastructure validation
- Extension system improvements
- Secrets management
- Workspace operations
- Cache management system

Plugin System:
- Interactive form plugin (inquire)
- KCL integration plugin
- Performance optimization plugins
- Plugin registration system

Utilities:
- Build and distribution scripts
- Installation procedures
- Testing utilities
- Development tools

Documentation:
- Library module documentation
- Extension API guides
- Plugin usage guides
- Service management documentation

All changes are backward compatible. No breaking changes.
2025-12-11 21:57:05 +00:00

535 lines
15 KiB
Plaintext

#!/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
}
}