provisioning-core/nulib/orchestration/servers/formula.nu

244 lines
12 KiB
Text
Raw Normal View History

use tools/nickel/process.nu [ncl-eval-soft]
use primitives/io/interface.nu [_print]
use platform/settings/loader.nu [find_get_settings]
use utils.nu [read_infra_taskserv_states]
def _prov-root []: nothing -> string {
$env.PROVISIONING? | default "/usr/local/provisioning"
}
def _infra-dir [settings: record]: nothing -> string {
let infra_path = ($settings | get -o infra_path | default "")
let infra_name = ($settings | get -o infra | default "")
if ($infra_name | is-not-empty) {
$infra_path | path join $infra_name
} else {
$infra_path
}
}
def _icon [state: string]: nothing -> string {
match $state {
"completed" => "✅"
"failed" => "❌"
"running" => "🔄"
"partial" => "🟡"
_ => "⏳"
}
}
def _load-formula [infra_dir: string, hostname: string]: nothing -> any {
let dag_path = ($infra_dir | path join "dag.ncl")
if not ($dag_path | path exists) { return null }
let dag = (ncl-eval-soft $dag_path [(_prov-root)] null)
if $dag == null { return null }
let formulas = ($dag | get -o formulas | default [])
$formulas | where { |f| ($f | get -o server | default "") == $hostname } | get 0? | default null
}
def _load-components [infra_dir: string]: nothing -> record {
let settings_path = ($infra_dir | path join "settings.ncl")
if not ($settings_path | path exists) { return {} }
let data = (ncl-eval-soft $settings_path [(_prov-root)] {})
$data | get -o components | default {}
}
# Resolve component record for a formula node, trying profile-derived key variants.
def _comp-for-node [components: record, ts_name: string, profile: string]: nothing -> record {
let keys_to_try = if ($profile | is-not-empty) {
let prof_key = ($profile | str replace "-" "_")
[$"($ts_name)_($prof_key)", $ts_name]
} else {
[$ts_name]
}
for $k in $keys_to_try {
if $k in ($components | columns) { return ($components | get $k) }
}
{}
}
# Return taskserv-mode components that explicitly target the given server (e.g. cilium, hetzner_csi).
def _targeted-taskservs-for [components: record, hostname: string]: nothing -> list<record> {
$components | items {|name comp|
if ($comp | get -o mode | default "cluster") == "taskserv" and ($comp | get -o target | default "") == $hostname {
[{ name: $name, version: ($comp | get -o version | default ""), comp: $comp }]
} else { [] }
} | flatten
}
# Return all cluster-mode components (K8s workloads deployed via just deploy from the CP).
def _k8s-workload-comps [components: record]: nothing -> list<record> {
$components | items {|name comp|
if ($comp | get -o mode | default "cluster") == "cluster" {
[{ name: $name, version: ($comp | get -o version | default ""), comp: $comp }]
} else { [] }
} | flatten
}
# True when the formula contains a kubernetes control-plane node — used to identify CP formulas.
def _is-cp-formula [formula: record]: nothing -> bool {
$formula.nodes | any { |n|
let ts = ($n | get -o taskserv | default {})
(($ts | get -o name | default "") == "kubernetes") and (($ts | get -o profile | default "") == "control-plane")
}
}
# Show the formula node chain, component settings, and available commands for a server.
export def on_server_formula [
hostname?: string
--infra (-i): string = ""
--settings (-s): string = ""
]: nothing -> nothing {
let resolved = (find_get_settings --infra $infra --settings $settings false true)
let infra_dir = (_infra-dir $resolved)
let infra_name = ($resolved | get -o infra | default "")
let all_servers = ($resolved.data.servers? | default [])
let hostname = if ($hostname | is-not-empty) {
$hostname
} else {
let choices = ($all_servers | where { |s| not ($s | get -o not_use | default false) } | each {|s| $s.hostname})
if ($choices | is-empty) { print "No servers configured"; return }
let sel = ($choices | input list "Select server:")
if ($sel | is-empty) { return }
$sel
}
let formula = (_load-formula $infra_dir $hostname)
if $formula == null {
print $"❌ No formula found for '($hostname)' in ($infra_dir)/dag.ncl"
return
}
let components = (_load-components $infra_dir)
let dag_states = (read_infra_taskserv_states $infra_dir)
let node_states = if $hostname in ($dag_states | columns) { $dag_states | get $hostname } else { [] }
print $"== ($formula.id) =="
print $"server: ($hostname)"
let desc = ($formula | get -o description | default "")
if ($desc | is-not-empty) { print $"chain: ($desc)" }
print ""
# ── Node chain ───────────────────────────────────────────────────────────
let total = ($formula.nodes | length)
let completed = ($node_states | where { $in.state == "completed" } | length)
print $"Nodes — ($completed)/($total) completed:"
print ""
for node in $formula.nodes {
let ts_name = ($node | get -o taskserv | default {} | get -o name | default $node.id)
let profile = ($node | get -o taskserv | default {} | get -o profile | default "")
let deps = ($node | get -o depends_on | default [] | each {|d| $d.node_id } | str join ", ")
let on_err = ($node | get -o on_error | default "Stop" | into string)
let node_key = ($ts_name | str replace --all "_" "-")
let st_entry = ($node_states | where { |e| $e.name == $node.id or $e.name == $node_key } | get 0?)
let state = ($st_entry | get -o state | default "pending")
let icon = (_icon $state)
let comp = (_comp-for-node $components $ts_name $profile)
let comp_ver = ($comp | get -o version | default "")
let profile_tag = if ($profile | is-not-empty) { $" profile:($profile)" } else { "" }
let ver_tag = if ($comp_ver | is-not-empty) { $" v($comp_ver)" } else { "" }
let dep_tag = if ($deps | is-not-empty) { $" after:($deps)" } else { "" }
let err_tag = if $on_err == "Continue" { " on_error:Continue" } else { "" }
print $" ($icon) ($node.id | fill -a l -w 22) [($ts_name)]($profile_tag)($ver_tag)($dep_tag)($err_tag)"
# Show key configurable settings — skip structural/meta fields, cap line length
if ($comp | columns | is-not-empty) {
let meta = ["mode" "version" "target" "operations" "requires" "provides" "live_check" "pod_selector" "namespace" "profile"]
let pairs = ($comp | items {|k v| {k: $k, v: $v}}
| where { |p| not ($p.k in $meta) }
| each { |p|
let s = if ($p.v | describe) in ["bool" "int" "float" "string"] {
$p.v | into string
} else {
$p.v | to nuon
}
if ($s | str length) <= 72 { $"($p.k)=($s)" } else { "" }
}
| where { is-not-empty })
if ($pairs | is-not-empty) {
print $" settings: ($pairs | str join ' ')"
}
}
}
let is_cp = (_is-cp-formula $formula)
let targeted_ts = (_targeted-taskservs-for $components $hostname)
let k8s_workloads = if $is_cp { (_k8s-workload-comps $components) } else { [] }
# ── Targeted taskservs on this server (cilium, hetzner_csi, longhorn…) ──
if ($targeted_ts | is-not-empty) {
print ""
print "Targeted taskservs on this server:"
for c in $targeted_ts {
let ver = if ($c.version | is-not-empty) { $" v($c.version)" } else { "" }
print $" ⚙️ ($c.name)($ver)"
}
}
# ── K8s workload components (cluster-mode, CP only) ──────────────────────
if ($k8s_workloads | is-not-empty) {
print ""
print "K8s workload components (cluster-mode — deployed from this CP via just deploy):"
for c in $k8s_workloads {
let ver = if ($c.version | is-not-empty) { $" v($c.version)" } else { "" }
print $" 🎯 ($c.name)($ver)"
}
}
# ── Commands ─────────────────────────────────────────────────────────────
print ""
print "── Commands ──────────────────────────────────────────────────────────"
print ""
print "Formula nodes (taskserv-mode — executed by DAG, no op governance):"
print $" install node: prvng component install <node> --infra ($infra_name) --server ($hostname)"
print $" update node: prvng component update <node> --infra ($infra_name) --server ($hostname)"
print $" delete node: prvng component delete <node> --infra ($infra_name) --server ($hostname)"
print $" full formula: prvng server create ($hostname) --infra ($infra_name) --wait"
if ($targeted_ts | is-not-empty) {
print ""
print "Targeted taskservs (taskserv-mode, run from CP, no op governance):"
for c in $targeted_ts {
print $" prvng component install ($c.name) --infra ($infra_name)"
print $" prvng component update ($c.name) --infra ($infra_name)"
print $" prvng component delete ($c.name) --infra ($infra_name)"
}
}
if ($k8s_workloads | is-not-empty) {
print ""
print "K8s workload components (cluster-mode — two execution paths):"
print ""
print " With governance ← USE THIS in production"
print " jj captures a snapshot before + after, op record enables rollback and audit."
for c in $k8s_workloads {
print $" just deploy ($c.name) install \"<intent>\" # first deploy"
print $" just deploy ($c.name) update \"<intent>\" # apply config changes + rolling restart"
print $" just deploy ($c.name) restart \"<intent>\" # pod cycle only"
print $" just deploy ($c.name) delete \"<intent>\" # teardown workload, keep data (pvc/ns)"
print $" just rollback <op-id> # revert to pre-op snapshot"
print $" just log ($c.name) # full op history"
print $" just render ($c.name) [update] [bundle] # inspect manifests locally, no SSH"
}
print ""
print " Direct ← dev/debug only, no audit trail"
for c in $k8s_workloads {
print $" prvng component install ($c.name) --infra ($infra_name)"
print $" prvng component update ($c.name) --infra ($infra_name)"
print $" prvng component delete ($c.name) --infra ($infra_name)"
}
print ""
print " ⚡ Why just deploy? Preflight (--check) runs first — fails fast before any state"
print " changes. Then jj opens a change (snapshot of infra state), the op is recorded"
print " in infra/libre-wuji/ops/ with intent + timestamps. If something goes wrong,"
print " just rollback <op-id> restores the pre-op jj snapshot. prvng component runs"
print " the deploy directly with none of that — fine for testing, risky in prod."
}
}