provisioning-core/nulib/cli/component.nu
Jesús Pérez 6f0de7ae89
core: move cluster introspection helpers to domain/cluster (ADR-026 slice 2b)
Complete the cluster decomposition: the 23 introspection helpers (capacity,
kubectl/ssh summaries, live-check, badges) move from cli/components_cluster.nu to
domain/cluster/introspect.nu. cli/components_cluster.nu now holds ONLY the 5
`main cluster *` verbs (963 -> 452 lines). introspect.nu imports downward only
(tools/nickel, domain/cluster/access, platform/clients/ssh, primitives/logging).

Exported 4 previously-private helpers (fmt-gib, k8s-quantity-bytes,
node-operator-labels, cluster-top-legend) so the cli verbs can call them across
the module boundary; dropped a dead comp-dag-summary import. component.nu repoints
its 11 helper imports to introspect.nu.

Verified: module-load of all touched modules (no cycle, no upward import); ADR-026
topology clean across the whole cluster subsystem; cluster --help + component list
run. Concludes slices 2a+2b: cli/components.nu 3611 -> 2410 (-33%).
2026-07-10 07:24:20 +01:00

736 lines
37 KiB
Text

#!/usr/bin/env nu
# Thin entry for component commands.
# Bypasses full dispatcher — loads only components/mod.nu + targeted lib_provisioning.
# Mirrors the provisioning-taskserv.nu pattern for <1s startup.
export-env {
let lib_dirs_raw = ($env.NU_LIB_DIRS? | default "")
let current_lib_dirs = if ($lib_dirs_raw | type) == "string" {
if ($lib_dirs_raw | is-empty) { [] } else { ($lib_dirs_raw | split row ":") }
} else {
$lib_dirs_raw
}
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
$env.NU_LIB_DIRS = ([
"/opt/provisioning/core/nulib"
"/usr/local/provisioning/core/nulib"
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
}
use primitives/io/interface.nu [_ansi]
use cli/components.nu [
"main component list"
"main component info"
"main component mark-complete"
"main component bulk-mark-complete"
"main component drop-key"
"main component drop-server"
"main component state-check"
"main component state-normalize"
"main validate capabilities"
"main validate components"
"main component install"
"main component delete"
"main component update"
"main component reinstall"
"main component restart"
"main component backup"
"main component restore"
"main component check-updates"
"main component render"
comp-load-component-record
]
use tools/nickel/process.nu [comp-ncl-export]
use domain/cluster/access.nu [comp-kubectl-access]
use domain/workspace/resolve.nu [comp-state-server comp-ws-path comp-ws-infra]
use domain/cluster/introspect.nu [
comp-live-check
comp-pods-for
comp-ssh-pods
comp-ssh-k8s-data
comp-ssh-describe-pod
comp-load-server-info
comp-server-badge
comp-cluster-summary
comp-cluster-badge
comp-cache-last-sync
comp-settings-k0s-version
]
def state-color [s: string]: nothing -> string {
match $s {
"completed" => $"(_ansi green)($s)(_ansi reset)"
"running" => $"(_ansi yellow)($s)(_ansi reset)"
"failed" => $"(_ansi red)($s)(_ansi reset)"
"blocked" => $"(_ansi red)($s)(_ansi reset)"
"partial" => $"(_ansi yellow)($s)(_ansi reset)"
_ => $"(_ansi dark_gray)($s)(_ansi reset)"
}
}
def main [
...args: string
--workspace (-w): string = ""
--mode: string = ""
--out (-o): string = ""
--infra (-i): string = ""
--server (-s): string = ""
--op: string = "init" # render: which op's plan scripts to include
--bundle # render: also write <name>.tar.gz.b64
--check
--dry-run # alias for --check: validates bundle without submitting
--wait
--live
--ext
--json (-j)
--debug (-x)
--filter (-f): string = "" # substring on name, or col=val (state=failed, ns=monitoring)
]: nothing -> nothing {
if $debug { $env.PROVISIONING_DEBUG = "true" }
let effective_check = ($check or $dry_run)
# Args come in as: ["component", "ls"] or ["ls", "postgresql"] depending on dispatch
let rest = if (($args | length) > 0) and (($args | first) in ["component", "comp", "c"]) {
$args | skip 1
} else {
$args
}
let sub = ($rest | get 0? | default "")
let name = ($rest | get 1? | default "")
# Workspace resolution: explicit flag > active env > empty (ext_only view)
let ws = if ($workspace | is-not-empty) {
$workspace
} else {
$env.PROVISIONING_KLOUD? | default ""
}
let help_text = {
print "Component Management"
print "===================="
print ""
print "Usage: prvng component <subcommand> [options]"
print ""
print "Read operations:"
print " list [--workspace <ws>] [--live] Provisioning state per component (alias: ls, l)"
print " ll [--workspace <ws>] Same as list --live (alias for --live)"
print " lc [--workspace <ws>] Same as list --mode cluster (alias: list cluster)"
print " lcl [--workspace <ws>] Same as list --mode cluster --live (alias: lc + live)"
print " [--server <host>] Filter taskserv rows to one server"
print " [<host>] Positional server filter (same as --server)"
print " [--mode cluster|taskserv] Filter by mode"
print " [--filter <text>] Substring match on name (e.g. --filter prom)"
print " [--filter col=val] Per-column match: state=failed, ns=monitoring, srv=wrk-0"
print " [--json] Raw JSON output (flat table)"
print " show <name> [--workspace <ws>] [--live] Full definition + state history (alias: sh, info)"
print " status <name> [--workspace <ws>] [--live] Operational snapshot + state (alias: s, st)"
print ""
print "State column values:"
print " pending Task not yet started (or server never initialized)"
print " running Task in progress — or stuck if ended_at is empty"
print " completed Task finished successfully via orchestrator"
print " failed Task finished with error"
print " — Component not in any DAG formula and no state entry yet"
print ""
print "Write operations (require orchestrator):"
print " install <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
print " delete <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
print " update <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
print " reinstall <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
print " restart <name> [--workspace <ws>] [--server <host>] [--wait]"
print " backup <name> [--workspace <ws>] [--server <host>] [--wait]"
print " restore <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
print " check-updates <name> [--workspace <ws>] [--server <host>]"
print ""
print "State correction:"
print " mark-complete <name> [--server <host>] [--note <text>]"
print " Force state → completed with actor attribution. Use when:"
print " • Component deployed manually or by an agent without orchestrator callback"
print " • State stuck in 'running (ended_at empty) after successful deploy"
print " • Task completed via human intervention and needs verification record"
print " Alias: mc"
print ""
print " mark-complete-live [--workspace <ws>]"
print " Bulk mark-complete for all components where state=running AND live=healthy."
print " Fetches live k8s data (one SSH session) before deciding — only marks what"
print " is confirmed running by the live check strategy."
print " Alias: mcl"
print ""
print " mark-complete-all [--workspace <ws>]"
print " Bulk mark-complete for ALL components where state=running, no live check."
print " Use for components without live_check (os, vol_prepare, kubernetes, etc.)"
print " or when k8s is unreachable and state is known to be correct."
print " Alias: mca"
print ""
print " bulk-mark-complete [--include-failed] [--dry-run] [--note <text>]"
print " Single-write sweep: marks all state=running (ended_at empty) as completed."
print " --include-failed: also sweep state=failed entries (e.g. cilium/longhorn that"
print " failed on first attempt but are actually running — verify pods first)."
print " Alias: bmc"
print ""
print " state-check [--server <host>]"
print " Diagnose state incoherence: lists stale-running, failed-but-recovered,"
print " and duplicate kebab/snake keys. Use this first to understand what to fix."
print " Alias: sc"
print ""
print " drop-key <key> --server <host> [--dry-run]"
print " Remove a specific state key from a server's taskservs. Use to prune"
print " orphaned duplicate entries left by naming migrations (e.g. odoo-jesusperez"
print " coexisting with odoo_jesusperez)."
print " Alias: dk"
print ""
print " state-normalize [--dry-run]"
print " One-shot migration: merge all snake_case state keys into their kebab"
print " equivalents. Eliminates duplicate entries from previous naming inconsistency."
print " Run once, then verify with sc. Future writes always use kebab (fixed upstream)."
print " Alias: sn"
print ""
print "Flags:"
print " --live Fetch live pod status from k8s via SSH (k0s kubectl get pods)."
print " Shows actual pod counts — not a proxy for provisioning state."
print " --check / --dry-run Dry-run: validates bundle without submitting to orchestrator."
print " --wait Block until orchestrator task reaches terminal state."
print ""
print "Examples:"
print " prvng c l # provisioning state"
print " prvng c ll # + live pod status"
print " prvng c lc # cluster components only"
print " prvng c lcl # cluster components + live"
print " prvng c l libre-wuji-wrk-0 # taskserv for one server"
print " prvng c l --live --workspace libre-wuji"
print " prvng c mc postgresql --note 'verified: kubectl get sts -n data'"
print " prvng c install postgresql --wait"
print " prvng c sh postgresql --live"
}
match $sub {
"" => { do $help_text }
"list" | "ls" | "l" | "ll" | "lc" | "lcl" => {
let is_live = $live or ($sub == "ll") or ($sub == "lcl")
let effective_mode = if $sub in ["lc", "lcl"] {
"cluster"
} else if ($mode | is-not-empty) {
$mode
} else {
""
}
# Second positional arg is treated as a server filter for list ops.
let effective_server = if ($server | is-not-empty) { $server } else { $name }
let all_rows = with-env { PROVISIONING_NO_CACHE: "true" } {
if ($ws | is-not-empty) {
main component list --workspace $ws --mode $effective_mode --server $effective_server
} else {
main component list --mode $effective_mode --server $effective_server
}
}
let filtered_rows = if ($filter | is-not-empty) {
if ($filter | str contains "=") {
let parts = ($filter | split row "=" | take 2)
let col = ($parts | get 0)
let val = ($parts | get 1 | str downcase)
let col_map = { ns: "namespace", state: "state", deploy: "state", name: "name", srv: "server", server: "server" }
let real_col = ($col_map | get -o $col | default $col)
$all_rows | where { ($in | get -o $real_col | default "" | into string | str downcase) | str contains $val }
} else {
let term = ($filter | str downcase)
$all_rows | where { $in.name | str downcase | str contains $term }
}
} else {
$all_rows
}
let cluster_rows = ($filtered_rows | where mode == "cluster")
let taskserv_rows = ($filtered_rows | where mode == "taskserv")
# Single SSH session for all live checks (cluster + taskserv).
let ws_root_l = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
let infra_l = (comp-ws-infra $ws_root_l)
let state_root_l = if (($ws_root_l | path join "infra" $infra_l ".provisioning-state.ncl") | path exists) {
$ws_root_l | path join "infra" $infra_l
} else { $ws_root_l }
let ssh_srv = if $is_live { (comp-kubectl-access $ws_root_l $infra_l).host } else { "" }
let k8s_data = if $is_live {
let systemd_svcs = ($filtered_rows
| where { ($in | get -o live_check | default {} | get -o strategy | default "") == "systemd" }
| each { $in | get -o live_check | default {} | get -o service | default "" }
| where { $in | is-not-empty })
comp-ssh-k8s-data $ws_root_l $infra_l $ssh_srv --systemd-services $systemd_svcs
} else { {} }
# A taskserv's systemd service runs on the node it was deployed to — not the
# CP. The single CP gather above only sees cp-0's units, so a worker-only
# service (e.g. coredns-vpn-wrk1 on wrk-1) always shows inactive. Gather
# systemd per-server for taskserv rows whose server is not the CP, so each is
# checked on its own node. (live_check.scope is advisory; the deploy server is
# authoritative for where a taskserv's unit actually runs.)
let extra_systemd = if $is_live {
$taskserv_rows
| where { (($in | get -o live_check | default {} | get -o strategy | default "") == "systemd") and (($in.server? | default "") != $ssh_srv) and (($in.server? | default "") | is-not-empty) }
| group-by server
| items {|srv, rows|
let svcs = ($rows | each { $in | get -o live_check | default {} | get -o service | default "" } | where { $in | is-not-empty } | uniq)
{ server: $srv, systemd: ((comp-ssh-k8s-data $ws_root_l $infra_l $srv --systemd-services $svcs) | get -o systemd | default {}) }
}
| reduce --fold {} {|it, acc| $acc | insert $it.server $it.systemd }
} else { {} }
# If live data could not be fetched (SSH/kubectl unreachable), every row
# would misleadingly show "no pods". Warn once that it's a fetch failure,
# not pod absence.
if $is_live and ($k8s_data.pods | is-empty) and ($k8s_data.nodes | is-empty) {
print $"(_ansi yellow)⚠ live cluster data unavailable \(CP: ($ssh_srv | default 'unresolved'))\) — 'live' column reflects the failed fetch, not pod absence(_ansi reset)"
}
# Server info: cache by default, hcloud query when --live.
let srv_info = if ($taskserv_rows | is-not-empty) or ($cluster_rows | is-not-empty) {
if $is_live {
comp-load-server-info $ws_root_l $infra_l --live
} else {
comp-load-server-info $ws_root_l $infra_l
}
} else { {} }
if $json {
let components_out = ($filtered_rows | reject pod_selector live_check)
let servers_out = ($srv_info | items {|k v| {key: $k, value: $v}}
| reduce --fold {} {|it acc| $acc | insert $it.key $it.value})
{components: $components_out, servers: $servers_out} | to json | print
return
}
# ── Cluster section ──────────────────────────────────────────
if ($cluster_rows | is-not-empty) {
let _op_ws = (comp-ncl-export $ws_root_l $"infra/($infra_l)/settings.ncl"
| get -o settings | default {}
| get -o operator_workspace | default "")
let _cluster_header = if ($_op_ws | is-not-empty) {
$"(_ansi white_bold)CLUSTER COMPONENTS(_ansi reset) (_ansi dark_gray)→ hosted on(_ansi reset) (_ansi cyan)($_op_ws)(_ansi reset)"
} else {
$"(_ansi white_bold)CLUSTER COMPONENTS(_ansi reset)"
}
print $_cluster_header
print $"(_ansi dark_gray)────────────────────────────────────────────────────(_ansi reset)"
let cl_version = (comp-settings-k0s-version $ws_root_l $infra_l)
let cl_summary = if $is_live {
comp-cluster-summary $ws_root_l $infra_l $k8s_data $cl_version --live
} else {
comp-cluster-summary $ws_root_l $infra_l {} $cl_version
}
let cl_badge = (comp-cluster-badge $cl_summary)
if ($cl_badge | is-not-empty) {
print $" (_ansi dark_gray)($cl_badge)(_ansi reset)"
}
if $is_live {
$cluster_rows | each {|row|
let check = (comp-live-check $row.name $row $k8s_data)
let live_val = if $check.live == "—" {
$"(_ansi dark_gray)($check.detail)(_ansi reset)"
} else { $check.detail }
$row | select name namespace version class resources placement state | update state {|r| state-color $r.state} | rename --column {state: deploy} | insert live $live_val
} | table | print
} else {
$cluster_rows | select name namespace version class resources placement state | update state {|r| state-color $r.state} | rename --column {state: deploy} | table | print
}
print ""
}
# ── Taskserv section ─────────────────────────────────────────
if ($taskserv_rows | is-not-empty) {
print $"(_ansi white_bold)TASKSERV COMPONENTS(_ansi reset)"
let servers = ($taskserv_rows | get server | uniq | sort)
for srv in $servers {
print ""
let info = ($srv_info | get -o $srv | default {})
let badge = (comp-server-badge $info)
if ($badge | is-not-empty) {
print $" (_ansi white_bold)($srv)(_ansi reset) (_ansi dark_gray)($badge)(_ansi reset)"
} else {
print $" (_ansi white_bold)($srv)(_ansi reset)"
}
print $" (_ansi dark_gray)──────────────────────────────────────────────(_ansi reset)"
let srv_rows = ($taskserv_rows | where server == $srv | sort-by name)
if $is_live {
# Use this server's own systemd state (gathered per-server above)
# so a taskserv unit is checked on the node it runs on, not the CP.
let srv_k8s = ($k8s_data | upsert systemd (if ($srv == $ssh_srv) { ($k8s_data.systemd? | default {}) } else { ($extra_systemd | get -o $srv | default {}) }))
$srv_rows | each {|row|
let check = (comp-live-check $row.name $row $srv_k8s)
let live_val = if $check.live == "—" {
$"(_ansi dark_gray)($check.detail)(_ansi reset)"
} else { $check.detail }
{ name: $row.name, deploy: (state-color $row.state), live: $live_val }
} | table | str trim | lines | each {|ln| print $" ($ln)"}
} else {
$srv_rows
| select name state
| update state {|r| state-color $r.state}
| rename --column {state: deploy}
| table | str trim | lines | each {|ln| print $" ($ln)"}
}
}
print ""
}
if not $is_live {
let ts = (comp-cache-last-sync $ws_root_l $infra_l)
if ($ts | is-not-empty) {
print $"(_ansi dark_gray)from cache updated ($ts)(_ansi reset)"
}
}
}
"status" | "st" | "s" => {
if ($name | is-empty) {
print "Error: component status requires a name"
print "Usage: prvng component status <name> [--workspace <ws>]"
return
}
let ws_root_s = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
let infra_s = (comp-ws-infra $ws_root_s)
let info = (comp-load-component-record $name $ws_root_s $infra_s)
let dag = ($info.dag? | default [])
let overall = if ($dag | is-empty) {
"— (no DAG)"
} else if ($dag | all { $in.state == "completed" }) {
"✅ deployed"
} else if ($dag | any { $in.state == "running" }) {
"🔄 running"
} else if ($dag | any { $in.state == "failed" }) {
"❌ failed"
} else if ($dag | any { $in.state == "blocked" }) {
"⊘ blocked"
} else if ($dag | any { $in.state == "unknown" }) {
"? unknown (reset/reprovisioned)"
} else {
"⏳ pending"
}
print $"component: ($name)"
print $"state: ($overall)"
print $"mode: ($info.mode)"
print $"target: (if ($info.target | is-not-empty) { $info.target } else { '—' })"
print $"namespace: (if ($info.namespace | is-not-empty) { $info.namespace } else { '—' })"
print $"version: (if ($info.version | is-not-empty) { $info.version } else { '—' })"
let req_count = ($info.requires | length)
let ops_count = ($info.operations | length)
if $req_count > 0 { print $"requires: ($info.requires | str join ', ')" }
if $ops_count > 0 { print $"operations: ($ops_count) defined" }
if $live {
print ""
let dag_server = ($dag | get 0? | default null | get -o server | default "")
let k8s_data = (comp-ssh-k8s-data $ws_root_s $infra_s $dag_server)
let check = (comp-live-check $name $info $k8s_data)
let live_icon = match $check.live {
"healthy" => "✅",
"degraded" => "❌",
"starting" => "🔄",
"partial" => "⚠",
"not deployed" => "⊘",
_ => "—",
}
let live_label = if $check.live == "—" {
$"($live_icon) ($check.detail)"
} else {
$"($live_icon) ($check.live) ($check.detail)"
}
print $"live check: ($live_label)"
}
if ($dag | is-not-empty) {
print ""
print "DAG execution:"
for entry in $dag {
let state_icon = match $entry.state {
"completed" => "✅",
"running" => "🔄",
"failed" => "❌",
"blocked" => "⊘",
"unknown" => "?",
_ => "⏳",
}
let ts = if ($entry.ended_at | is-not-empty) {
$entry.ended_at | str replace "T" " " | str replace "Z" ""
} else if ($entry.started_at | is-not-empty) {
$"started ($entry.started_at | str replace 'T' ' ' | str replace 'Z' '')"
} else {
""
}
let blocker_suffix = if ($entry.blocker | is-not-empty) {
$" ← blocked by ($entry.blocker)"
} else { "" }
print $" ($state_icon) ($entry.formula | fill -a l -w 24) server: ($entry.server) ($entry.state | fill -a l -w 10) ($ts)($blocker_suffix)"
}
}
}
"show" | "info" | "sh" => {
if ($name | is-empty) {
print "Error: component show requires a name"
print "Usage: prvng component show <name> [--workspace <ws>]"
return
}
if ($ws | is-not-empty) {
main component info $name --workspace $ws
} else {
main component info $name
}
}
"install" => {
if ($name | is-empty) { print "Error: component install requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component install $name --workspace $ws --server $server --check=$effective_check --wait=$wait
} else {
main component install $name --server $server --check=$effective_check --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"delete" => {
if ($name | is-empty) { print "Error: component delete requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component delete $name --workspace $ws --server $server --check=$effective_check --wait=$wait
} else {
main component delete $name --server $server --check=$effective_check --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"update" => {
if ($name | is-empty) { print "Error: component update requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component update $name --workspace $ws --server $server --check=$effective_check --wait=$wait
} else {
main component update $name --server $server --check=$effective_check --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"reinstall" => {
if ($name | is-empty) { print "Error: component reinstall requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component reinstall $name --workspace $ws --server $server --check=$effective_check --wait=$wait
} else {
main component reinstall $name --server $server --check=$effective_check --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"restart" => {
if ($name | is-empty) { print "Error: component restart requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component restart $name --workspace $ws --server $server --wait=$wait
} else {
main component restart $name --server $server --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"backup" => {
if ($name | is-empty) { print "Error: component backup requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component backup $name --workspace $ws --server $server --wait=$wait
} else {
main component backup $name --server $server --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"restore" => {
if ($name | is-empty) { print "Error: component restore requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component restore $name --workspace $ws --server $server --check=$effective_check --wait=$wait
} else {
main component restore $name --server $server --check=$effective_check --wait=$wait
}
$task_ids | each {|id| print $"task: ($id)"}
}
"check-updates" => {
if ($name | is-empty) { print "Error: component check-updates requires a name"; return }
let task_ids = if ($ws | is-not-empty) {
main component check-updates $name --workspace $ws --server $server
} else {
main component check-updates $name --server $server
}
$task_ids | each {|id| print $"task: ($id)"}
}
"mark-complete" | "mc" => {
if ($name | is-empty) { print "Error: component mark-complete requires a name"; return }
if ($ws | is-not-empty) {
main component mark-complete $name --workspace $ws --server $server
} else {
main component mark-complete $name --server $server
}
}
"mark-complete-live" | "mcl" => {
let ws_root = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
let infra = (comp-ws-infra $ws_root)
let state_root = if (($ws_root | path join "infra" $infra ".provisioning-state.ncl") | path exists) {
$ws_root | path join "infra" $infra
} else { $ws_root }
let rows = with-env { PROVISIONING_NO_CACHE: "true" } {
if ($ws | is-not-empty) {
main component list --workspace $ws --mode $mode
} else {
main component list --mode $mode
}
}
let ssh_server = (comp-state-server $state_root)
let k8s_data = (comp-ssh-k8s-data $ws_root $infra $ssh_server)
let candidates = (
$rows
| where { $in.state in ["running", "failed"] }
| each {|row|
let check = (comp-live-check $row.name $row $k8s_data)
if $check.live == "healthy" {
[{ name: $row.name, server: ($row.server | default ""), prior_state: $row.state }]
} else { [] }
}
| flatten
)
if ($candidates | is-empty) {
print "No components with state=running/failed and live=healthy found."
return
}
print $"Marking ($candidates | length) components complete:"
for c in $candidates {
print $" → ($c.name) [($c.prior_state) → completed]"
let srv = if ($c.server | is-not-empty) { $c.server } else { $server }
try {
if ($ws | is-not-empty) {
main component mark-complete $c.name --workspace $ws --server $srv
} else {
main component mark-complete $c.name --server $srv
}
} catch {|e|
print $" (_ansi yellow)skipped: ($e.msg)(_ansi reset)"
}
}
}
"mark-complete-all" | "mca" => {
let ws_root = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
let infra = (comp-ws-infra $ws_root)
let state_root = if (($ws_root | path join "infra" $infra ".provisioning-state.ncl") | path exists) {
$ws_root | path join "infra" $infra
} else { $ws_root }
let rows = with-env { PROVISIONING_NO_CACHE: "true" } {
if ($ws | is-not-empty) {
main component list --workspace $ws --mode $mode
} else {
main component list --mode $mode
}
}
let ssh_server = (comp-state-server $state_root)
let candidates = (
$rows
| where { $in.state == "running" }
| each {|row|
[{ name: $row.name, server: ($row.server | default "") }]
}
| flatten
)
if ($candidates | is-empty) {
print "No components with state=running found."
return
}
print $"Marking ($candidates | length) components complete \(no live check\):"
for c in $candidates {
print $" → ($c.name)"
let srv = if ($c.server | is-not-empty) { $c.server } else { $ssh_server }
try {
if ($ws | is-not-empty) {
main component mark-complete $c.name --workspace $ws --server $srv
} else {
main component mark-complete $c.name --server $srv
}
} catch {|e|
print $" (_ansi yellow)skipped: ($e.msg)(_ansi reset)"
}
}
}
"bulk-mark-complete" | "bmc" => {
let include_failed = ($rest | any { $in == "--include-failed" })
let is_dry = ($dry_run or $check or ($rest | any { $in == "--dry-run" }))
if $is_dry {
if ($ws | is-not-empty) {
main component bulk-mark-complete --workspace $ws --infra $infra --dry-run
} else {
main component bulk-mark-complete --infra $infra --dry-run
}
} else if $include_failed {
if ($ws | is-not-empty) {
main component bulk-mark-complete --workspace $ws --infra $infra --include-failed
} else {
main component bulk-mark-complete --infra $infra --include-failed
}
} else {
if ($ws | is-not-empty) {
main component bulk-mark-complete --workspace $ws --infra $infra
} else {
main component bulk-mark-complete --infra $infra
}
}
}
"state-check" | "sc" => {
if ($ws | is-not-empty) {
main component state-check --workspace $ws --infra $infra --server $server
} else {
main component state-check --infra $infra --server $server
}
}
"state-normalize" | "sn" => {
let is_dry = ($dry_run or $check)
if $is_dry {
if ($ws | is-not-empty) {
main component state-normalize --workspace $ws --infra $infra --dry-run
} else {
main component state-normalize --infra $infra --dry-run
}
} else {
if ($ws | is-not-empty) {
main component state-normalize --workspace $ws --infra $infra
} else {
main component state-normalize --infra $infra
}
}
}
"drop-server" | "ds" => {
if ($name | is-empty) { print "Error: drop-server requires a hostname"; return }
let is_dry = ($dry_run or $check)
if $is_dry {
if ($ws | is-not-empty) {
main component drop-server $name --workspace $ws --infra $infra --dry-run
} else {
main component drop-server $name --infra $infra --dry-run
}
} else {
if ($ws | is-not-empty) {
main component drop-server $name --workspace $ws --infra $infra
} else {
main component drop-server $name --infra $infra
}
}
}
"drop-key" | "dk" => {
if ($name | is-empty) { print "Error: drop-key requires a key name"; return }
if ($server | is-empty) { print "Error: drop-key requires --server <hostname>"; return }
let is_dry = ($dry_run or $check)
if $is_dry {
if ($ws | is-not-empty) {
main component drop-key $name --server $server --workspace $ws --infra $infra --dry-run
} else {
main component drop-key $name --server $server --infra $infra --dry-run
}
} else {
if ($ws | is-not-empty) {
main component drop-key $name --server $server --workspace $ws --infra $infra
} else {
main component drop-key $name --server $server --infra $infra
}
}
}
"render" => {
if ($name | is-empty) { print "Error: component render requires a name"; return }
if $bundle {
main component render $name --op $op --workspace $ws --infra $infra --out $out --bundle
} else {
main component render $name --op $op --workspace $ws --infra $infra --out $out
}
}
"help" | "h" | "-h" | "--help" => { do $help_text }
_ => {
print $"Unknown component subcommand: ($sub)"
print "Run: prvng component help"
exit 1
}
}
}