244 lines
12 KiB
Text
244 lines
12 KiB
Text
|
|
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."
|
||
|
|
}
|
||
|
|
}
|