- 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.
231 lines
8.9 KiB
Text
231 lines
8.9 KiB
Text
use ../lib_provisioning/workspace *
|
|
use ../lib_provisioning/workspace/notation.nu *
|
|
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft]
|
|
|
|
# Resolve the provisioning root for --import-path resolution.
|
|
def provisioning-root [] : nothing -> string {
|
|
$env.PROVISIONING? | default "/usr/local/provisioning"
|
|
}
|
|
|
|
# Export a Nickel file as parsed JSON, or error with stderr context.
|
|
def nickel-export [path: string] : nothing -> record {
|
|
ncl-eval $path [(provisioning-root)]
|
|
}
|
|
|
|
# Show the workspace DAG composition for a given infra.
|
|
#
|
|
# Renders each formula_id with its depends_on edges, conditions, health gates,
|
|
# and parallel flag. Marks root and terminal nodes.
|
|
export def "main dag show" [
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
--infra (-i): string = "wuji" # Infra name
|
|
] : nothing -> nothing {
|
|
let ws_name = if ($workspace | is-not-empty) {
|
|
$workspace
|
|
} else {
|
|
let details = (get-active-workspace-details)
|
|
if ($details == null) {
|
|
error make { msg: "No active workspace. Pass --workspace or activate one first." }
|
|
}
|
|
$details.name
|
|
}
|
|
|
|
let ws_root = (get-workspace-path $ws_name)
|
|
if ($ws_root | is-empty) {
|
|
error make { msg: $"Workspace '($ws_name)' not found in registry." }
|
|
}
|
|
|
|
let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
|
|
if not ($dag_path | path exists) {
|
|
error make { msg: $"dag.ncl not found at ($dag_path)" }
|
|
}
|
|
|
|
let dag = (nickel-export $dag_path)
|
|
let formulas = $dag.composition.formulas
|
|
|
|
# Determine roots (no depends_on) and terminals (not depended upon by others).
|
|
let all_dep_targets = ($formulas | each { |e| $e.depends_on | each { |d| $d.formula_id } } | flatten)
|
|
let roots = ($formulas | where ($it.depends_on | length) == 0 | each { |e| $e.formula_id })
|
|
let terminals = ($formulas | where { |e| not ($all_dep_targets | any { |d| $d == $e.formula_id }) } | each { |e| $e.formula_id })
|
|
|
|
print $"DAG: ($dag.workspace) / ($dag.infra)"
|
|
print ""
|
|
|
|
for entry in $formulas {
|
|
let is_root = ($roots | any { |r| $r == $entry.formula_id })
|
|
let is_terminal = ($terminals | any { |t| $t == $entry.formula_id })
|
|
let tags = ([
|
|
(if $is_root { "[root]" } else { "" })
|
|
(if $is_terminal { "[terminal]" } else { "" })
|
|
(if $entry.parallel { "[parallel]" } else { "" })
|
|
] | where ($it | is-not-empty) | str join " ")
|
|
|
|
print $" ($entry.formula_id) ($tags)"
|
|
|
|
if ($entry.depends_on | length) > 0 {
|
|
for dep in $entry.depends_on {
|
|
print $" └─ depends_on: ($dep.formula_id) [($dep.condition)]"
|
|
}
|
|
}
|
|
|
|
if "health_gate" in $entry and ($entry.health_gate != null) {
|
|
let g = $entry.health_gate
|
|
print $" └─ health_gate: ($g.check_cmd) | expect=($g.expect) timeout=($g.timeout_ms)ms retries=($g.retries)"
|
|
}
|
|
|
|
print ""
|
|
}
|
|
}
|
|
|
|
# Validate dag.ncl against its Nickel schema and cross-check formula_ids against servers.ncl.
|
|
export def "main dag validate" [
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
--infra (-i): string = "wuji" # Infra name
|
|
] : nothing -> nothing {
|
|
let ws_name = if ($workspace | is-not-empty) {
|
|
$workspace
|
|
} else {
|
|
let details = (get-active-workspace-details)
|
|
if ($details == null) {
|
|
error make { msg: "No active workspace. Pass --workspace or activate one first." }
|
|
}
|
|
$details.name
|
|
}
|
|
|
|
let ws_root = (get-workspace-path $ws_name)
|
|
if ($ws_root | is-empty) {
|
|
error make { msg: $"Workspace '($ws_name)' not found in registry." }
|
|
}
|
|
|
|
let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
|
|
let servers_path = ($ws_root | path join "infra" $infra "servers.ncl")
|
|
let prov_root = (provisioning-root)
|
|
|
|
mut passed = true
|
|
|
|
# Step 1: schema + contract validation via nickel export
|
|
print " [1/3] Nickel schema + WorkspaceComposition contract ..."
|
|
let dag_data = (ncl-eval-soft $dag_path [$prov_root] null)
|
|
if ($dag_data | is-not-empty) {
|
|
print " PASS"
|
|
} else {
|
|
print " FAIL: nickel export failed or empty"
|
|
$passed = false
|
|
}
|
|
|
|
# Step 2: load servers.ncl formula IDs
|
|
print " [2/3] Cross-check formula_ids against servers.ncl ..."
|
|
let servers_data = (ncl-eval-soft $servers_path [$prov_root] null)
|
|
if ($servers_data | is-empty) {
|
|
print " SKIP (servers.ncl export failed)"
|
|
} else if ($dag_data | is-not-empty) {
|
|
let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id })
|
|
let server_ids = ($servers_data | get formulas | each { |f| $f.id })
|
|
let dangling = ($dag_ids | where { |id| not ($server_ids | any { |sid| $sid == $id }) })
|
|
if ($dangling | length) == 0 {
|
|
print " PASS"
|
|
} else {
|
|
print $" FAIL: dag.ncl references unknown formula_ids: ($dangling | str join ', ')"
|
|
$passed = false
|
|
}
|
|
}
|
|
|
|
# Step 3: check all formulas in servers.ncl are covered by dag.ncl
|
|
print " [3/3] Coverage — all servers.ncl formulas present in dag.ncl ..."
|
|
if ($servers_data | is-not-empty) and ($dag_data | is-not-empty) {
|
|
let dag_ids = ($dag_data | get composition.formulas | each { |e| $e.formula_id })
|
|
let server_ids = ($servers_data | get formulas | each { |f| $f.id })
|
|
let uncovered = ($server_ids | where { |id| not ($dag_ids | any { |did| $did == $id }) })
|
|
if ($uncovered | length) == 0 {
|
|
print " PASS"
|
|
} else {
|
|
print $" WARN: servers.ncl formulas not in dag.ncl (intentional?): ($uncovered | str join ', ')"
|
|
}
|
|
}
|
|
|
|
print ""
|
|
if $passed {
|
|
print "dag validate: OK"
|
|
} else {
|
|
print "dag validate: FAILED"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# Export dag.ncl in various formats.
|
|
export def "main dag export" [
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
--infra (-i): string = "wuji" # Infra name
|
|
--format (-f): string = "json" # Output format: json, dot, cytoscape-json
|
|
] : nothing -> nothing {
|
|
let ws_name = if ($workspace | is-not-empty) {
|
|
$workspace
|
|
} else {
|
|
let details = (get-active-workspace-details)
|
|
if ($details == null) {
|
|
error make { msg: "No active workspace. Pass --workspace or activate one first." }
|
|
}
|
|
$details.name
|
|
}
|
|
|
|
let ws_root = (get-workspace-path $ws_name)
|
|
if ($ws_root | is-empty) {
|
|
error make { msg: $"Workspace '($ws_name)' not found in registry." }
|
|
}
|
|
|
|
let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
|
|
let dag = (nickel-export $dag_path)
|
|
|
|
match $format {
|
|
"json" => {
|
|
print ($dag | to json)
|
|
}
|
|
"dot" => {
|
|
print "digraph dag {"
|
|
print " rankdir=LR;"
|
|
for entry in $dag.composition.formulas {
|
|
let shape = if ($entry.depends_on | length) == 0 { "shape=invhouse" } else { "shape=box" }
|
|
print $" \"($entry.formula_id)\" [($shape)];"
|
|
for dep in $entry.depends_on {
|
|
let label = $dep.condition
|
|
print $" \"($dep.formula_id)\" -> \"($entry.formula_id)\" [label=\"($label)\"];"
|
|
}
|
|
if "health_gate" in $entry and ($entry.health_gate != null) and (($entry.depends_on | length) > 0) {
|
|
let gate_id = $"health_gate__($entry.depends_on.0.formula_id)__($entry.formula_id)"
|
|
print $" \"($gate_id)\" [shape=hexagon label=\"health gate\"];"
|
|
print $" \"($entry.depends_on.0.formula_id)\" -> \"($gate_id)\" [style=dashed];"
|
|
print $" \"($gate_id)\" -> \"($entry.formula_id)\" [style=dashed];"
|
|
}
|
|
}
|
|
print "}"
|
|
}
|
|
"cytoscape-json" => {
|
|
let nodes = ($dag.composition.formulas | each { |e|
|
|
{
|
|
data: {
|
|
id: $e.formula_id,
|
|
label: $e.formula_id,
|
|
shape: "rectangle",
|
|
parallel: $e.parallel,
|
|
}
|
|
}
|
|
})
|
|
let edges = ($dag.composition.formulas | each { |e|
|
|
$e.depends_on | each { |dep|
|
|
{
|
|
data: {
|
|
id: $"($dep.formula_id)__($e.formula_id)",
|
|
source: $dep.formula_id,
|
|
target: $e.formula_id,
|
|
label: $dep.condition,
|
|
}
|
|
}
|
|
}
|
|
} | flatten)
|
|
print ({ elements: { nodes: $nodes, edges: $edges } } | to json)
|
|
}
|
|
_ => {
|
|
error make { msg: $"Unknown format '($format)'. Valid: json, dot, cytoscape-json" }
|
|
}
|
|
}
|
|
}
|