prvng_core/nulib/components/mod.nu
Jesús Pérez 894046ef5a
feat(core): three-layer DAG, unified component arch, commands-registry cache, Nushell 0.112.2 migration
- DAG architecture: `dag show/validate/export` (nulib/main_provisioning/dag.nu),
    config loader (lib_provisioning/config/loader/dag.nu), taskserv dag-executor.
    Backed by schemas/lib/dag/*.ncl; orchestrator emits NATS events via
    WorkspaceComposition::into_workflow. See ADR-020, ADR-021.
  - Unified Component Architecture: components/mod.nu, main_provisioning/
    {components,workflow,extensions,ontoref-queries}.nu. Full workflow engine with
    topological sort and NATS subject emission. Blocks A-H complete (libre-daoshi).
  - Commands-registry: nulib/commands-registry.ncl (Nickel source, 314 lines) +
    JSON cache at ~/.cache/provisioning/commands-registry.json rebuilt on source
    change. cli/provisioning fast-path alias expansion avoids cold Nu startup.
    ADDING_COMMANDS.md documents new-command workflow.
  - Platform service manager: service-manager.nu (+573), startup.nu (+611),
    service-check.nu (+255); autostart/bootstrap/health/target refactored.
  - Nushell 0.112.2 migration: removed all try/catch and bash redirections;
    external commands prefixed with ^; type signatures enforced. Driven by
    scripts/refactor-try-catch{,-simplified}.nu.
  - TTY stack: removed shlib/*-tty.sh; replaced by cli/tty-dispatch.sh,
    tty-filter.sh, tty-commands.conf.
  - New domain modules: images/ (golden image lifecycle), workspace/{state,sync}.nu,
    main_provisioning/{bootstrap,cluster-deploy,fip,state}.nu, commands/{state,
    build,integrations/auth,utilities/alias}.nu, platform.nu expanded (+874).
  - Config loader overhaul: loader/core.nu slimmed (-759), cache/core.nu
    refactored (-454), removed legacy loaders/file_loader.nu (-330).
  - Thirteen new provisioning-<domain>.nu top-level modules for bash dispatcher.
  - Tests: test_workspace_state.nu (+351); updates to test_oci_registry,
    test_services.
  - README + CHANGELOG updated.
2026-04-17 04:27:33 +01:00

312 lines
15 KiB
Text

#!/usr/bin/env nu
# Component management module — list, show, status for extensions/components.
#
# Two perspectives per component:
# extension — what exists in extensions/components/{name}/ (metadata, modes, contract)
# workspace — how it's instantiated in infra/{ws}/components/{name}.ncl
#
# Ontology data (FSM state, edges) is read via ontoref when available (defensive).
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft, default-ncl-paths]
# Resolve the extensions/components/ base path.
def _comp-ext-base []: nothing -> string {
let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let p = ($prov | path join "extensions" | path join "components")
if ($p | path exists) { return $p }
}
""
}
# Resolve the workspace root for a given workspace name.
# Checks PROVISIONING_KLOUD_PATH env, then walks known workspace directories.
def _ws-root [workspace: string]: nothing -> string {
if ($workspace | is-empty) { return "" }
let from_env = ($env.PROVISIONING_KLOUD_PATH? | default "")
if ($from_env | is-not-empty) and ($from_env | path basename) == $workspace {
return $from_env
}
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) {
let ws_root = ($prov | path dirname | path join "workspaces" | path join $workspace)
if ($ws_root | path exists) { return $ws_root }
}
""
}
# Export a Nickel file to a record. Returns null on failure.
# Uses default-ncl-paths to match the daemon's cache key derivation.
def _ncl-export [file_path: string]: nothing -> any {
let ws_root = ($file_path | path dirname | path dirname | path dirname)
ncl-eval-soft $file_path (default-ncl-paths $ws_root) null
}
# Read FSM dimension for a component from state.ncl via ontoref or raw NCL export.
def _read-fsm-state [name: string, ws_root: string]: nothing -> record {
let dim_id = $"($name)-status"
# Try ontoref first (richer output)
let onto_result = (do {
^ontoref describe state $dim_id --fmt json --workspace $ws_root
} | complete)
if $onto_result.exit_code == 0 {
let parsed = (do { $onto_result.stdout | from json } | complete)
if $parsed.exit_code == 0 { return $parsed.stdout }
}
# Fallback: export state.ncl and filter
let state_path = ($ws_root | path join ".ontology" | path join "state.ncl")
if not ($state_path | path exists) { return {} }
let prov = ($env.PROVISIONING? | default "")
let state_data = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) {})
if ($state_data | is-empty) { return {} }
let dims = ($state_data | get -o dimensions | default [])
$dims | where {|d| ($d | get -o id | default "") == $dim_id } | get 0? | default {}
}
# Read ontology node and edges for a component from core.ncl.
def _read-onto-node [name: string, ws_root: string]: nothing -> record {
let core_path = ($ws_root | path join ".ontology" | path join "core.ncl")
if not ($core_path | path exists) { return { node: null, edges_from: [], edges_to: [] } }
let prov = ($env.PROVISIONING? | default "")
let data = (ncl-eval-soft $core_path (default-ncl-paths $ws_root) null)
if $data == null { return { node: null, edges_from: [], edges_to: [] } }
let nodes = ($data | get -o nodes | default [])
let edges = ($data | get -o edges | default [])
let node = ($nodes | where {|n| ($n | get -o id | default "") == $name } | get 0? | default null)
let edges_from = ($edges | where {|e| ($e | get -o from | default "") == $name })
let edges_to = ($edges | where {|e| ($e | get -o to | default "") == $name })
{ node: $node, edges_from: $edges_from, edges_to: $edges_to }
}
# List all components from extensions/components/ with optional mode filter and workspace state.
export def component-list [mode: string, workspace: string]: nothing -> nothing {
let base = (_comp-ext-base)
if ($base | is-empty) or not ($base | path exists) {
print "❌ extensions/components/ not found. Set PROVISIONING env var."
return
}
let ws_root = (_ws-root $workspace)
let show_state = ($ws_root | is-not-empty)
mut rows = []
for item in (ls $base | where type == "dir") {
let name = ($item.name | path basename)
let meta_p = ($item.name | path join "metadata.ncl")
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
# Mode filter
if ($mode | is-not-empty) and ($mode not-in $modes) { continue }
let state = if $show_state {
let dim = (_read-fsm-state $name $ws_root)
if ($dim | is-empty) { "—" } else {
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "")
if ($des | is-not-empty) and $cur != $des { $"($cur) → ($des)" } else { $cur }
}
} else { "—" }
$rows = ($rows | append {
name: $name
mode: ($modes | str join "·")
state: $state
version: $version
})
}
if ($rows | is-empty) {
print "No components found."
return
}
let header = if $show_state { $"Components [workspace: ($workspace)]" } else { "Components [extension catalog]" }
print $header
print "────────────────────────────────────────────────────────────"
$rows | table
}
# Show full details for a named component.
export def component-show [name: string, workspace: string, ext_only: bool]: nothing -> nothing {
let base = (_comp-ext-base)
let ext_dir = ($base | path join $name)
if not ($ext_dir | path exists) {
print $"❌ Component '($name)' not found in extensions/components/"
return
}
let meta_p = ($ext_dir | path join "metadata.ncl")
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
# Extension section
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
let tags = if $meta != null { $meta | get -o tags | default [] | str join " · " } else { "" }
# Defaults (requires/provides/operations from nickel/defaults.ncl)
let defaults_p = ($ext_dir | path join "nickel" | path join "defaults.ncl")
let defaults = if ($defaults_p | path exists) { _ncl-export $defaults_p } else { null }
let def_rec = if $defaults != null { $defaults | get -o $name | default {} } else { {} }
let requires = ($def_rec | get -o requires | default {})
let provides = ($def_rec | get -o provides | default {})
let operations = ($def_rec | get -o operations | default {})
print $"┌─ ($name | str upcase) ─────────────────────────────────"
print $"│ ($desc)"
print $"├────────────────────────────────────────────────────────"
let modes_str = ($modes | str join " · ")
print $"│ VERSION ($version)"
print $"│ MODES ($modes_str)"
if ($tags | is-not-empty) { print $"│ TAGS ($tags)" }
# REQUIRES
let req_storage = ($requires | get -o storage | default null)
let req_ports = ($requires | get -o ports | default [])
let req_creds = ($requires | get -o credentials | default [])
if $req_storage != null or ($req_ports | is-not-empty) or ($req_creds | is-not-empty) {
print "├─── REQUIRES ───────────────────────────────────────────"
if $req_storage != null {
let persist_label = if ($req_storage.persistent? | default false) { "persistent" } else { "ephemeral" }
let stor_size = ($req_storage.size? | default "?")
print $"│ storage ($stor_size) ($persist_label)"
}
for p in $req_ports {
let pport = ($p.port? | default 0 | into string)
let pproto = ($p.protocol? | default "TCP")
let pexpose = ($p.exposure? | default "internal")
print $"│ port ($pport)/($pproto) \(($pexpose)\)"
}
if ($req_creds | is-not-empty) {
let creds_str = ($req_creds | str join " · ")
print $"│ creds ($creds_str)"
}
}
# PROVIDES
let prov_svc = ($provides | get -o service | default "")
let prov_port = ($provides | get -o port | default null)
let prov_dbs = ($provides | get -o databases | default [])
if ($prov_svc | is-not-empty) or $prov_port != null or ($prov_dbs | is-not-empty) {
print "├─── PROVIDES ───────────────────────────────────────────"
if ($prov_svc | is-not-empty) and $prov_port != null {
print $"│ service ($prov_svc):($prov_port)"
} else if ($prov_svc | is-not-empty) {
print $"│ service ($prov_svc)"
}
if ($prov_dbs | is-not-empty) {
let dbs_str = ($prov_dbs | str join " · ")
print $"│ databases ($dbs_str)"
}
}
# OPERATIONS
let ops_enabled = ($operations | transpose k v | where v == true | each {|r| $r.k })
if ($ops_enabled | is-not-empty) {
let ops_str = ($ops_enabled | str join " · ")
print "├─── OPERATIONS ─────────────────────────────────────────"
print $"│ ($ops_str)"
}
if not $ext_only and ($workspace | is-not-empty) {
let ws_root = (_ws-root $workspace)
if ($ws_root | is-not-empty) {
# Workspace instance
let comp_p = ($ws_root | path join "infra" | path join $workspace | path join "components" | path join $"($name).ncl")
let comp_data = if ($comp_p | path exists) { _ncl-export $comp_p } else { null }
let inst = if $comp_data != null { $comp_data | get -o $name | default {} } else { {} }
let inst_mode = ($inst | get -o mode | default "")
let inst_ns = ($inst | get -o namespace | default "")
let inst_tgt = ($inst | get -o target | default "")
print "├─── WORKSPACE INSTANCE ─────────────────────────────────"
if ($inst_mode | is-not-empty) { print $"│ mode ($inst_mode)" }
if ($inst_ns | is-not-empty) { print $"│ namespace ($inst_ns)" }
if ($inst_tgt | is-not-empty) { print $"│ target ($inst_tgt)" }
# FSM state
let dim = (_read-fsm-state $name $ws_root)
if not ($dim | is-empty) {
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "")
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
let blk_short = ($blk | str substring 0..80)
print "├─── STATE ───────────────────────────────────────────"
print $"│ current ($cur)"
if ($des | is-not-empty) { print $"│ desired ($des)" }
if ($blk | is-not-empty) { print $"│ blocker ($blk_short)" }
}
# Ontology
let onto = (_read-onto-node $name $ws_root)
if $onto.node != null {
let node = $onto.node
let node_lvl = ($node.level? | default "?")
let node_pole = ($node.pole? | default "?")
print "├─── ONTOLOGY ────────────────────────────────────────"
print $"│ node ($name) \(($node_lvl) / ($node_pole)\)"
let arts = ($node | get -o artifact_paths | default [])
if ($arts | is-not-empty) {
let arts_str = ($arts | str join " · ")
print $"│ artifacts ($arts_str)"
}
let adrs = ($node | get -o adrs | default [])
if ($adrs | is-not-empty) {
let adrs_str = ($adrs | str join " · ")
print $"│ adrs ($adrs_str)"
}
if ($onto.edges_from | is-not-empty) {
let consumers = ($onto.edges_from | each {|e|
let eto = ($e | get -o to | default "?")
let ekind = ($e | get -o kind | default "")
$"($eto) \(($ekind)\)"
} | str join " · ")
print $"│ used-by ($consumers)"
}
if ($onto.edges_to | is-not-empty) {
let uses = ($onto.edges_to | each {|e|
let efrom = ($e | get -o from | default "?")
let ekind = ($e | get -o kind | default "")
$"($efrom) \(($ekind)\)"
} | str join " · ")
print $"│ uses ($uses)"
}
}
}
}
print "└────────────────────────────────────────────────────────"
}
# Show only FSM state for a component.
export def component-status [name: string, workspace: string]: nothing -> nothing {
if ($workspace | is-empty) {
print "❌ --workspace required for status"
return
}
let ws_root = (_ws-root $workspace)
if ($ws_root | is-empty) {
print $"❌ Workspace '($workspace)' not found"
return
}
let dim = (_read-fsm-state $name $ws_root)
if ($dim | is-empty) {
print $"No FSM dimension found for '($name)-status' in ($workspace)"
return
}
let cur = ($dim | get -o current_state | default "—")
let des = ($dim | get -o desired_state | default "—")
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
let cat = ($dim | get -o transitions | default [] | get 0? | default {} | get -o catalyst | default "")
print $"($name) — FSM state [($workspace)]"
print $" current: ($cur)"
print $" desired: ($des)"
if ($blk | is-not-empty) { print $" blocker: ($blk)" }
if ($cat | is-not-empty) { print $" catalyst: ($cat)" }
}