provisioning-core/nulib/platform/service_manager.nu

524 lines
17 KiB
Text
Raw Permalink Normal View History

# Platform Service Manager - Service management for deployment
# Handles loading deployment configuration and service management
use tools/nickel/process.nu [ncl-eval ncl-eval-soft]
use platform/user/config.nu [get-active-workspace-details get-platform-config-dir]
# Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
# Returns the normalized name (e.g., "provisioning_daemon" → "daemon")
export def normalize-service-name [service_name: string] {
if ($service_name | str starts-with "provisioning-") {
$service_name | str replace "provisioning-" ""
} else if ($service_name | str starts-with "provisioning_") {
$service_name | str replace "provisioning_" ""
} else {
$service_name
}
}
# Load deployment mode configuration from Nickel
export def load-deployment-mode [] {
# Prefer explicit env override; otherwise the OS-aware platform dir.
let possible_paths = [
($env.PROVISIONING_USER_PLATFORM? | default null),
(get-platform-config-dir),
]
let config_file = (
$possible_paths
| where { |p| $p != null }
| each { |p| $p | path expand | path join "deployment-mode.ncl" }
| where { |p| $p | path exists }
| get 0?
)
if ($config_file == null) {
error make {msg: "Deployment mode file not found in any of the expected locations"}
}
let import_path = ($env.PROVISIONING? | default "")
let import_paths = if ($import_path | is-not-empty) { [$import_path] } else { [] }
ncl-eval $config_file $import_paths
}
# Load individual service configuration
export def load-service-config [service_name: string] {
# Prefer explicit env override; otherwise the OS-aware platform dir.
let possible_bases = [
($env.PROVISIONING_USER_PLATFORM? | default null),
(get-platform-config-dir),
]
# Try to find the config file
let config_file = (
$possible_bases
| where { |b| $b != null }
| each { |base|
# Try both the underscore and dash versions
let base_expanded = ($base | path expand)
let path1 = ($base_expanded | path join "config" ($"($service_name).ncl"))
let path2 = ($base_expanded | path join "config" ($"($service_name | str replace "_" "-").ncl"))
if ($path1 | path exists) {
$path1
} else if ($path2 | path exists) {
$path2
} else {
null
}
}
| where { |p| $p != null }
| get 0?
)
if ($config_file == null) {
return null
}
ncl-eval-soft $config_file [] null
}
# Get the port for a service from its per-service NCL config file.
export def get-service-port [service_name: string] {
let config = (load-service-config $service_name)
if ($config == null) {
return "?"
}
let service_key = $service_name | str replace "-" "_"
if ($config | get --optional $service_key) != null {
let service_config = ($config | get $service_key)
if ($service_config | get --optional "server") != null {
if ($service_config.server | get --optional "port") != null {
return ($service_config.server.port | into string)
}
}
if ($service_config | get --optional "build") != null {
if ($service_config.build | get --optional "port") != null {
return ($service_config.build.port | into string)
}
}
if ($service_config | get --optional "http") != null {
if ($service_config.http | get --optional "port") != null {
return ($service_config.http.port | into string)
}
}
if ($service_config | get --optional "port") != null {
return ($service_config.port | into string)
}
}
"?"
}
# Start required services based on deployment configuration
export def start-required-services [] {
let deployment = (load-deployment-mode)
# Get enabled services from deployment config
let services = $deployment.services
let all_service_names = ($services | columns)
# Filter to enabled services
let enabled_services = (
$all_service_names
| where {|name|
let config = ($services | get $name)
($config | get --optional "enabled" | default false)
}
)
if ($enabled_services | length) == 0 {
print "⚠ No services enabled in deployment-mode.ncl"
return
}
# Get current running processes once
let running_processes = (^ps aux)
# Start each enabled service
for service_name in $enabled_services {
let port = (get-service-port $service_name)
let normalized_name = (normalize-service-name $service_name)
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
let is_running = ($running_processes | str contains $binary_name)
# Check if binary exists
let home = ($env.HOME? | default "~" | path expand)
let binary_path = ($home | path join ".local/bin" $binary_name)
if not ($binary_path | path exists) {
print $"✗ ($service_name) binary not found"
continue
}
# If already running, just report it
if $is_running {
let status_msg = $"((ansi green))started((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($status_msg)"
continue
}
# Start in background
print $"→ Starting ($service_name)..."
let log_dir = ($home | path join ".provisioning/logs")
(do { mkdir ($log_dir) } | ignore)
let log_file = ($log_dir | path join $"($service_name).log")
# Set environment variables for service
let platform_path = (get-platform-config-dir)
let provisioning_path = ($home | path join ".local/bin/provisioning")
# Build start command (only orchestrator accepts --provisioning-path)
let start_cmd = if ($normalized_name == "orchestrator") {
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
} else {
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &"
}
(^sh -c $start_cmd | ignore)
sleep 2sec
let started_msg = $"((ansi green))started((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($started_msg)"
}
# ncl-sync: Nickel config cache daemon — always started, independent of deployment mode.
ncl-sync-start
print ""
}
# Start specific services by name
# Usage: start-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
export def start-services [service_list: list<string>] {
# Parse service names (handle comma-separated strings)
let services = (
$service_list
| each { |item| $item | split row "," | each { |s| $s | str trim } }
| flatten
| where { |s| ($s | is-not-empty) }
)
if ($services | length) == 0 {
print "⚠ No services specified"
return
}
# Start each service
for service_name in $services {
let normalized_name = (normalize-service-name $service_name)
let port = (get-service-port $normalized_name)
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
let is_running = ((^pgrep -f $"[/]($binary_name)$" | complete).exit_code == 0)
# Check if binary exists
let home = ($env.HOME? | default "~" | path expand)
let binary_path = ($home | path join ".local/bin" $binary_name)
if not ($binary_path | path exists) {
print $"✗ ($service_name) binary not found"
continue
}
# If already running, just report it
if $is_running {
let status_msg = $"((ansi green))started((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($status_msg)"
continue
}
# Start in background
print $"→ Starting ($service_name)..."
let log_dir = ($home | path join ".provisioning/logs")
(do { mkdir ($log_dir) } | ignore)
let log_file = ($log_dir | path join $"($service_name).log")
# Set environment variables for service
let platform_path = (get-platform-config-dir)
let provisioning_path = ($home | path join ".local/bin/provisioning")
# Build start command (only orchestrator accepts --provisioning-path)
let start_cmd = if ($normalized_name == "orchestrator") {
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
} else {
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &"
}
(^sh -c $start_cmd | ignore)
sleep 2sec
let started_msg = $"((ansi green))started((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($started_msg)"
}
ncl-sync-start
print ""
}
# Stop specific services by name
# Usage: stop-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
export def stop-services [service_list: list<string>] {
# Parse service names (handle comma-separated strings)
let services = (
$service_list
| each { |item| $item | split row "," | each { |s| $s | str trim } }
| flatten
| where { |s| ($s | is-not-empty) }
)
if ($services | length) == 0 {
print "⚠ No services specified"
return
}
# Stop each service
for service_name in $services {
# Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
let normalized_name = (
if ($service_name | str starts-with "provisioning-") {
$service_name | str replace "provisioning-" ""
} else if ($service_name | str starts-with "provisioning_") {
$service_name | str replace "provisioning_" ""
} else {
$service_name
}
)
let port = (get-service-port $normalized_name)
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
let is_running = ((^pgrep -f $"[/]($binary_name)$" | complete).exit_code == 0)
if $is_running {
(^sh -c $"pkill -f '($binary_name)' 2>/dev/null || true") | ignore
sleep 500ms
let stopped_msg = $"((ansi red))stopped((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
} else {
let stopped_msg = $"((ansi red))already stopped((ansi reset))"
print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
}
}
print ""
}
# Check if a port is listening (health check for external services)
export def is-port-listening [port: number] {
let uname_result = (do { ^uname -s } | complete)
let os_type = (if $uname_result.exit_code == 0 { $uname_result.stdout | str trim } else { "Linux" })
if $os_type == "Darwin" {
# macOS: use lsof to check listening ports
# Pattern matches both "*:PORT" and "127.0.0.1:PORT" formats
let check = (do { ^lsof -i -P -n } | complete)
if $check.exit_code == 0 {
let port_str = $"($port)"
$check.stdout | str contains $"($port_str)" | if $in { true } else { false }
} else {
false
}
} else {
# Linux: use netstat to check listening ports
let check = (do { ^netstat -tuln } | complete)
if $check.exit_code == 0 {
$check.stdout | str contains $":$port"
} else {
false
}
}
}
# Get external services from user configuration file
export def get-external-services [] {
# Prefer explicit env override; otherwise the OS-aware platform dir.
let possible_bases = [
($env.PROVISIONING_USER_PLATFORM? | default null),
(get-platform-config-dir),
]
# Try to find the external services config file
let external_services_file = (
$possible_bases
| where { |b| $b != null }
| each { |base| ($base | path expand | path join "config/external-services.ncl") }
| where { |p| $p | path exists }
| get 0?
)
if ($external_services_file == null) {
return []
}
ncl-eval-soft $external_services_file [] [] | default []
}
# Start nats-server as a child process for solo mode.
# Requires nats-server to be in PATH.
# Returns a record with {pid, port, jetstream_dir} on success.
export def nats_start [config: record]: nothing -> record {
let port = ($config.port? | default 4222)
let data_dir = ($env.HOME | path join ".local/share/provisioning/nats")
let js_dir = ($config.jetstream_store_dir? | default $data_dir)
let mk_result = (do { ^mkdir -p $js_dir } | complete)
if ($mk_result.exit_code != 0) {
error make {msg: $"Failed to create NATS data dir ($js_dir): ($mk_result.stderr)"}
}
# Spawn nats-server in background — nohup keeps it alive after this shell exits
let cmd = $"nohup nats-server -js -sd ($js_dir) -p ($port) >/dev/null 2>&1 &"
let start_result = (do { ^sh -c $cmd } | complete)
if ($start_result.exit_code != 0) {
error make {msg: $"nats-server failed to start: ($start_result.stderr)"}
}
# Poll for readiness — up to 10 seconds in 500ms increments
let ready = (
1..20
| each { |_i|
sleep 500ms
(nats_health {port: $port})
}
| where { |r| $r }
| length
| $in > 0
)
if (not $ready) {
error make {msg: "nats-server did not become ready within 10 seconds"}
}
let pid_result = (do { ^pgrep -f $"nats-server" } | complete)
let pid = (if ($pid_result.exit_code == 0) { $pid_result.stdout | lines | get 0? | default "0" | into int } else { 0 })
{pid: $pid, port: $port, jetstream_dir: $js_dir}
}
# Stop the nats-server process.
export def nats_stop [config: record]: nothing -> nothing {
let port = ($config.port? | default 4222)
let kill_result = (do { ^pkill -f "nats-server" } | complete)
if ($kill_result.exit_code != 0) {
print $"Warning: nats-server on port ($port) was not running or could not be stopped"
}
}
# Check if nats-server is accepting TCP connections on the configured port.
# Returns true if healthy, false otherwise.
export def nats_health [config: record]: nothing -> bool {
let port = ($config.port? | default 4222)
let check = (do { ^nc -z -w 1 127.0.0.1 $port } | complete)
$check.exit_code == 0
}
# ============================================================================
# ncl-sync daemon management
# ============================================================================
def ncl-sync-cache-dir []: nothing -> string {
if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
# Walk up from PWD to find workspace root
let pwd = $env.PWD
let ws_pwd = if ($pwd | path join "infra" | path exists) or ($pwd | path join "config" "provisioning.ncl" | path exists) or ($pwd | path join ".ontology" | path exists) {
$pwd
} else { "" }
if ($ws_pwd | is-not-empty) { return ($ws_pwd | path join ".ncl-cache") }
# Fallback to active workspace from user_config
let details = (get-active-workspace-details)
if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
return ($details.path | path join ".ncl-cache")
}
let home = ($env.HOME? | default "~" | path expand)
$home | path join ".cache" "provisioning" "config-cache"
}
def ncl-sync-bin []: nothing -> string {
let home = ($env.HOME? | default "~" | path expand)
$home | path join ".local" "bin" "provisioning-ncl-sync"
}
def ncl-sync-running []: nothing -> bool {
(do { ^ps aux } | complete).stdout | str contains "provisioning-ncl-sync"
}
# Check that a path has workspace markers (infra/, config/provisioning.ncl, or .ontology/).
def is-workspace-dir [path: string]: nothing -> bool {
if ($path | is-empty) or (not ($path | path exists)) { return false }
let has_infra = ($path | path join "infra" | path exists)
let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
let has_onto = ($path | path join ".ontology" | path exists)
$has_infra or $has_config or $has_onto
}
# Walk up from `path` until a workspace root is found or we reach filesystem root.
def find-workspace-up [path: string]: nothing -> string {
if ($path | is-empty) or $path == "/" { return "" }
if (is-workspace-dir $path) { return $path }
let parent = ($path | path dirname)
if $parent == $path { return "" }
find-workspace-up $parent
}
# Start the ncl-sync daemon if not already running.
# Workspace resolution priority:
# 1. $NCL_CACHE_DIR's parent (explicit override)
# 2. Walk up from PWD until a workspace root is found
# 3. get-active-workspace-details from user_config.yaml (if it's a valid workspace path)
# 4. skip (avoid watching HOME or random dirs)
export def ncl-sync-start []: nothing -> nothing {
if (ncl-sync-running) { return }
let bin = (ncl-sync-bin)
if not ($bin | path exists) { return }
let from_pwd = (find-workspace-up $env.PWD)
let details = (get-active-workspace-details)
let from_config = if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
$details.path
} else { "" }
let ws_path = if ($from_pwd | is-not-empty) {
$from_pwd
} else if (is-workspace-dir $from_config) {
$from_config
} else {
""
}
if ($ws_path | is-empty) {
print "→ ncl-sync: no workspace detected in PWD tree or user_config — skipping"
return
}
let home = ($env.HOME? | default "~" | path expand)
let log_dir = ($home | path join ".provisioning" "logs")
(do { mkdir $log_dir } | ignore)
let log_file = ($log_dir | path join "ncl-sync.log")
let cmd = $"nohup ($bin) daemon --workspace \"($ws_path)\" >>\"($log_file)\" 2>&1 &"
(^sh -c $cmd | ignore)
sleep 500ms
print $"→ ncl-sync started \(workspace: ($ws_path)\)"
}
# Stop all ncl-sync daemon instances.
export def ncl-sync-stop []: nothing -> nothing {
(do { ^pkill -f "provisioning-ncl-sync" } | complete | ignore)
}
# Status record for ncl-sync daemon.
export def ncl-sync-status []: nothing -> record {
let running = (ncl-sync-running)
{
service: "ncl-sync",
running: $running,
}
}