prvng_core/nulib/main_provisioning/components.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

256 lines
11 KiB
Text

use ../lib_provisioning/workspace *
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft, default-ncl-paths]
# Resolve the provisioning root for --import-path resolution.
def comp-prov-root []: nothing -> string {
$env.PROVISIONING? | default "/usr/local/provisioning"
}
# Export a Nickel file as parsed JSON. Uses default-ncl-paths to match the daemon's
# cache key derivation — otherwise every call misses and re-runs nickel export cold.
def comp-ncl-export [ws_root: string, rel_path: string]: nothing -> record {
let full_path = ($ws_root | path join $rel_path)
ncl-eval $full_path (default-ncl-paths $ws_root)
}
# Resolve workspace name: explicit --workspace flag or active workspace.
def comp-resolve-workspace [workspace: string]: nothing -> string {
if ($workspace | is-not-empty) {
return $workspace
}
let details = (get-active-workspace-details)
if ($details == null) {
error make { msg: "No active workspace — pass --workspace or activate one first" }
}
$details.name
}
# Validate cluster capabilities against real infrastructure state.
#
# Exports infra/{infra}/capabilities.ncl from the workspace and compares declared
# capabilities (storage_classes, ingress_class) against live kubectl output.
# Returns a table of check / expected / actual / status rows.
#
# Usage:
# provisioning validate capabilities --workspace libre-daoshi --infra wuji
export def "main validate capabilities" [
--workspace (-w): string # Workspace name (default: active)
--infra (-i): string = "wuji" # Infra name
]: nothing -> table<check: string, expected: string, actual: string, status: string> {
let ws_name = (comp-resolve-workspace $workspace)
let ws_root = (get-workspace-path $ws_name)
if ($ws_root | is-empty) {
error make { msg: $"Workspace '($ws_name)' not found in registry." }
}
let caps_path = ($ws_root | path join "infra" $infra "capabilities.ncl")
if not ($caps_path | path exists) {
error make { msg: $"capabilities.ncl not found at ($caps_path)" }
}
let caps = (comp-ncl-export $ws_root ($"infra/($infra)/capabilities.ncl"))
mut rows: list<record<check: string, expected: string, actual: string, status: string>> = []
# Check storage classes
let declared_sc = ($caps | get -o provides | default {} | get -o storage_classes | default [] | each { $in | into string })
if ($declared_sc | is-not-empty) {
let sc_result = (do { ^kubectl get sc --no-headers -o custom-columns=NAME:.metadata.name } | complete)
let actual_sc = if $sc_result.exit_code == 0 {
$sc_result.stdout | lines | where { $in | is-not-empty }
} else {
[]
}
for sc in $declared_sc {
let found = ($actual_sc | any { $in == $sc })
$rows = ($rows | append {
check: "storage_class",
expected: $sc,
actual: (if $found { $sc } else { "<not found>" }),
status: (if $found { "ok" } else { "MISSING" }),
})
}
}
# Check ingress class
let declared_ic = ($caps | get -o provides | default {} | get -o ingress_class | default "")
if ($declared_ic | is-not-empty) {
let ic_result = (do { ^kubectl get ingressclass --no-headers -o custom-columns=NAME:.metadata.name } | complete)
let actual_ic = if $ic_result.exit_code == 0 {
$ic_result.stdout | lines | where { $in | is-not-empty }
} else {
[]
}
let found = ($actual_ic | any { $in == $declared_ic })
$rows = ($rows | append {
check: "ingress_class",
expected: $declared_ic,
actual: (if $found { $declared_ic } else { "<not found>" }),
status: (if $found { "ok" } else { "MISSING" }),
})
}
$rows
}
# Validate component configuration against workspace capabilities and server inventory.
#
# Exports infra/{infra}/settings.ncl and checks each component:
# - taskserv mode: verifies the target server exists in the servers map.
# - cluster mode: verifies the storage_class (if declared) is in capabilities.storage_classes.
# Returns a table of component / check / status / detail rows.
#
# Usage:
# provisioning validate components --workspace libre-daoshi --infra wuji
export def "main validate components" [
--workspace (-w): string # Workspace name (default: active)
--infra (-i): string = "wuji" # Infra name
]: nothing -> table<component: string, check: string, status: string, detail: string> {
let ws_name = (comp-resolve-workspace $workspace)
let ws_root = (get-workspace-path $ws_name)
if ($ws_root | is-empty) {
error make { msg: $"Workspace '($ws_name)' not found in registry." }
}
let settings = (comp-ncl-export $ws_root ($"infra/($infra)/settings.ncl"))
let components = ($settings | get -o components | default {})
# Load capabilities for storage_class cross-check (best-effort: skip if absent).
let caps_path = ($ws_root | path join "infra" $infra "capabilities.ncl")
let caps_sc: list<string> = if ($caps_path | path exists) {
let c = (comp-ncl-export $ws_root ($"infra/($infra)/capabilities.ncl"))
$c | get -o provides | default {} | get -o storage_classes | default [] | each { $in | into string }
} else {
[]
}
# Load servers for taskserv target validation (best-effort).
let servers_path = ($ws_root | path join "infra" $infra "servers.ncl")
let server_names: list<string> = if ($servers_path | path exists) {
ncl-eval-soft $servers_path (default-ncl-paths $ws_root) {} | get -o servers | default {} | columns
} else {
[]
}
mut rows: list<record<component: string, check: string, status: string, detail: string>> = []
let comp_names = ($components | columns)
for comp_name in $comp_names {
let comp = ($components | get $comp_name)
let mode = ($comp | get -o mode | default "cluster")
if $mode == "taskserv" {
let target = ($comp | get -o target | default "")
if ($target | is-empty) {
$rows = ($rows | append { component: $comp_name, check: "target_server", status: "WARN", detail: "mode=taskserv but no target specified" })
} else if ($server_names | is-empty) {
$rows = ($rows | append { component: $comp_name, check: "target_server", status: "SKIP", detail: $"servers.ncl not available — cannot verify '($target)'" })
} else {
let found = ($server_names | any { $in == $target })
$rows = ($rows | append {
component: $comp_name,
check: "target_server",
status: (if $found { "ok" } else { "MISSING" }),
detail: (if $found { $"target '($target)' exists" } else { $"target '($target)' not found in servers" }),
})
}
} else if $mode == "cluster" {
let sc = ($comp | get -o storage_class | default "")
if ($sc | is-not-empty) {
if ($caps_sc | is-empty) {
$rows = ($rows | append { component: $comp_name, check: "storage_class", status: "SKIP", detail: "capabilities.ncl not available" })
} else {
let found = ($caps_sc | any { $in == $sc })
$rows = ($rows | append {
component: $comp_name,
check: "storage_class",
status: (if $found { "ok" } else { "MISSING" }),
detail: (if $found { $"storage_class '($sc)' available" } else { $"storage_class '($sc)' not in capabilities" }),
})
}
}
}
# Always emit a baseline row even when no sub-checks apply.
if ($rows | where component == $comp_name | is-empty) {
$rows = ($rows | append { component: $comp_name, check: "declared", status: "ok", detail: $"mode=($mode)" })
}
}
$rows
}
# List all components declared in the workspace infra settings.
#
# Reads infra/{infra}/settings.ncl and renders each component with its name,
# mode, target or namespace, and version (if available in the component config).
# Returns a table of name / mode / target / namespace / version rows.
#
# Usage:
# provisioning component list --workspace libre-daoshi --infra wuji
export def "main component list" [
--workspace (-w): string # Workspace name (default: active)
--infra (-i): string = "wuji" # Infra name
]: nothing -> table<name: string, mode: string, target: string, namespace: string, version: string> {
let ws_name = (comp-resolve-workspace $workspace)
let ws_root = (get-workspace-path $ws_name)
if ($ws_root | is-empty) {
error make { msg: $"Workspace '($ws_name)' not found in registry." }
}
let settings = (comp-ncl-export $ws_root ($"infra/($infra)/settings.ncl"))
let components = ($settings | get -o components | default {})
$components | columns | each { |comp_name|
let comp = ($components | get $comp_name)
{
name: $comp_name,
mode: ($comp | get -o mode | default "cluster"),
target: ($comp | get -o target | default ""),
namespace: ($comp | get -o namespace | default ""),
version: ($comp | get -o version | default ""),
}
}
}
# Show the full unified view of a single component declaration.
#
# Exports infra/{infra}/components/{name}.ncl from the workspace. If that file
# does not exist, falls back to the component entry in settings.ncl.
# Returns a record with mode, target, namespace, requires, provides, and operations.
#
# Usage:
# provisioning component info postgresql --workspace libre-daoshi --infra wuji
export def "main component info" [
name: string # Component name
--workspace (-w): string # Workspace name (default: active)
--infra (-i): string = "wuji" # Infra name
]: nothing -> record {
let ws_name = (comp-resolve-workspace $workspace)
let ws_root = (get-workspace-path $ws_name)
if ($ws_root | is-empty) {
error make { msg: $"Workspace '($ws_name)' not found in registry." }
}
# Prefer the per-component NCL file; fall back to settings.ncl entry.
let comp_ncl_path = ($ws_root | path join "infra" $infra "components" $"($name).ncl")
let comp = if ($comp_ncl_path | path exists) {
comp-ncl-export $ws_root ($"infra/($infra)/components/($name).ncl")
} else {
let settings = (comp-ncl-export $ws_root ($"infra/($infra)/settings.ncl"))
let components = ($settings | get -o components | default {})
if not ($name in ($components | columns)) {
error make { msg: $"Component '($name)' not declared in infra/($infra)/settings.ncl and no per-component NCL found at ($comp_ncl_path)" }
}
$components | get $name
}
{
mode: ($comp | get -o mode | default "cluster"),
target: ($comp | get -o target | default ""),
namespace: ($comp | get -o namespace | default ""),
version: ($comp | get -o version | default ""),
requires: ($comp | get -o requires | default []),
provides: ($comp | get -o provides | default {}),
operations: ($comp | get -o operations | default []),
}
}