- 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.
325 lines
13 KiB
Text
325 lines
13 KiB
Text
use ../lib_provisioning/user/config.nu [get-workspace-path, get-active-workspace-details]
|
|
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft, default-ncl-paths]
|
|
|
|
# Resolve provisioning root from env with default fallback.
|
|
def oq-prov-root []: nothing -> string {
|
|
$env.PROVISIONING? | default "/usr/local/provisioning"
|
|
}
|
|
|
|
# Export a Nickel file as parsed JSON using workspace + provisioning import paths.
|
|
def oq-ncl-export [ws_root: string, full_path: string]: nothing -> record {
|
|
ncl-eval $full_path (default-ncl-paths $ws_root)
|
|
}
|
|
|
|
# Resolve workspace name from optional arg or active workspace.
|
|
def oq-resolve-ws [workspace: string]: nothing -> record {
|
|
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) or ($ws_root == null) {
|
|
error make { msg: $"Workspace '($ws_name)' not found in registry." }
|
|
}
|
|
{ name: $ws_name, root: $ws_root }
|
|
}
|
|
|
|
# Detect infra subdirectory: first dir under infra/ that contains settings.ncl.
|
|
def oq-detect-infra [ws_root: string]: nothing -> string {
|
|
let result = (do { ^bash -c $"ls -1d ($ws_root)/infra/*/settings.ncl 2>/dev/null | head -1" } | complete)
|
|
if $result.exit_code != 0 or ($result.stdout | str trim | is-empty) {
|
|
error make { msg: $"No infra/*/settings.ncl found under ($ws_root)" }
|
|
}
|
|
let parts = ($result.stdout | str trim | path split)
|
|
# path: ws_root/infra/<name>/settings.ncl — index -2 is infra name.
|
|
$parts | get ($parts | length | $in - 2)
|
|
}
|
|
|
|
# Collect all workflow *.ncl files under infra/{infra}/workflows/.
|
|
def oq-collect-workflows [ws_root: string]: nothing -> list {
|
|
let result = (do { ^bash -c $"ls ($ws_root)/infra/*/workflows/*.ncl 2>/dev/null" } | complete)
|
|
if $result.exit_code != 0 or ($result.stdout | str trim | is-empty) {
|
|
return []
|
|
}
|
|
$result.stdout | lines | where { $in | str trim | is-not-empty }
|
|
}
|
|
|
|
# Load settings.ncl components for the auto-detected infra.
|
|
def oq-load-components [ws_root: string]: nothing -> record {
|
|
let infra = (oq-detect-infra $ws_root)
|
|
let path = ($ws_root | path join "infra" $infra "settings.ncl")
|
|
if not ($path | path exists) {
|
|
return {}
|
|
}
|
|
let exported = (oq-ncl-export $ws_root $path)
|
|
$exported | get -o components | default {}
|
|
}
|
|
|
|
# Show the unified view of a component: config, FSM dimension state, and ontology consumers.
|
|
#
|
|
# Reads infra/{infra}/components/{name}.ncl for config, .ontology/state.ncl for dimension
|
|
# state, and .ontology/core.ncl for edges referencing this component.
|
|
export def "main describe component" [
|
|
name: string # Component name (e.g. postgresql, forgejo)
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
] : nothing -> record {
|
|
let ws = (oq-resolve-ws $workspace)
|
|
let ws_root = $ws.root
|
|
|
|
let infra = (oq-detect-infra $ws_root)
|
|
let comp_path = ($ws_root | path join "infra" $infra "components" $"($name).ncl")
|
|
let settings_path = ($ws_root | path join "infra" $infra "settings.ncl")
|
|
|
|
# Component source config from its own NCL file.
|
|
let source_cfg = if ($comp_path | path exists) {
|
|
(oq-ncl-export $ws_root $comp_path) | get -o $name | default {}
|
|
} else if ($settings_path | path exists) {
|
|
let settings = (oq-ncl-export $ws_root $settings_path)
|
|
$settings | get -o components | default {} | get -o $name | default {}
|
|
} else {
|
|
{}
|
|
}
|
|
|
|
let mode = ($source_cfg | get -o mode | default "unknown" | into string | str replace "'" "")
|
|
let requires = ($source_cfg | get -o requires | default {})
|
|
let provides = ($source_cfg | get -o provides | default {})
|
|
let operations = ($source_cfg | get -o operations | default {})
|
|
|
|
# Extension path.
|
|
let prov_root = (oq-prov-root)
|
|
let ext_path = ($prov_root | path join "extensions/components" $name)
|
|
|
|
# FSM dimension: look for dimension id matching "{name}-status".
|
|
let state_path = ($ws_root | path join ".ontology" "state.ncl")
|
|
let fsm_state = if ($state_path | path exists) {
|
|
let state_data = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) null)
|
|
if ($state_data | is-not-empty) {
|
|
let dims = ($state_data | get -o dimensions | default [])
|
|
let dim_id = $"($name)-status"
|
|
let dim = ($dims | where {|d| $d.id == $dim_id})
|
|
if ($dim | is-empty) {
|
|
{ dimension: null, current_state: "unknown", desired_state: "unknown" }
|
|
} else {
|
|
let d = ($dim | first)
|
|
{
|
|
dimension: $dim_id,
|
|
current_state: ($d | get -o current_state | default "unknown"),
|
|
desired_state: ($d | get -o desired_state | default "unknown"),
|
|
}
|
|
}
|
|
} else {
|
|
{ dimension: null, current_state: "unknown", desired_state: "unknown" }
|
|
}
|
|
} else {
|
|
{ dimension: null, current_state: "unknown", desired_state: "unknown" }
|
|
}
|
|
|
|
# Ontology consumers: edges in core.ncl that reference this component name.
|
|
let core_path = ($ws_root | path join ".ontology" "core.ncl")
|
|
let consumers = if ($core_path | path exists) {
|
|
let core_data = (ncl-eval-soft $core_path (default-ncl-paths $ws_root) null)
|
|
if ($core_data | is-not-empty) {
|
|
let edges = ($core_data | get -o edges | default [])
|
|
$edges | where {|e|
|
|
($e | get -o from | default "") == $name or ($e | get -o to | default "") == $name
|
|
} | each {|e| { from: ($e | get -o from | default ""), to: ($e | get -o to | default ""), kind: ($e | get -o kind | default "") }}
|
|
} else { [] }
|
|
} else { [] }
|
|
|
|
{
|
|
name: $name,
|
|
mode: $mode,
|
|
requires: $requires,
|
|
provides: $provides,
|
|
operations: $operations,
|
|
state: $fsm_state,
|
|
consumers: $consumers,
|
|
extension_path: $ext_path,
|
|
}
|
|
}
|
|
|
|
# List all components that expose database services.
|
|
#
|
|
# Filters components where provides.databases is non-empty, returning a flat table
|
|
# with one row per component.
|
|
export def "main describe databases" [
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
] : nothing -> table {
|
|
let ws = (oq-resolve-ws $workspace)
|
|
let ws_root = $ws.root
|
|
let components = (oq-load-components $ws_root)
|
|
|
|
$components | columns | each {|comp_name|
|
|
let comp = ($components | get $comp_name)
|
|
let provides = ($comp | get -o provides | default {})
|
|
let databases = ($provides | get -o databases | default [])
|
|
if ($databases | is-not-empty) {
|
|
let port = ($provides | get -o port | default ($comp | get -o port | default 0))
|
|
let requires = ($comp | get -o requires | default {})
|
|
let ns_raw = ($comp | get -o namespace | default "default")
|
|
{
|
|
component: $comp_name,
|
|
databases: ($databases | str join ", "),
|
|
port: $port,
|
|
namespace: $ns_raw,
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
} | where { $in != null }
|
|
}
|
|
|
|
# List all components deployed to a specific Kubernetes namespace.
|
|
export def "main describe namespace" [
|
|
namespace: string # Kubernetes namespace to filter on
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
] : nothing -> table {
|
|
let ws = (oq-resolve-ws $workspace)
|
|
let ws_root = $ws.root
|
|
let components = (oq-load-components $ws_root)
|
|
|
|
$components | columns | each {|comp_name|
|
|
let comp = ($components | get $comp_name)
|
|
let comp_ns = ($comp | get -o namespace | default "")
|
|
if $comp_ns == $namespace {
|
|
let mode_raw = ($comp | get -o mode | default "unknown" | into string | str replace "'" "")
|
|
let port = ($comp | get -o port | default ($comp | get -o requires | default {} | get -o ports | default [] | first | default {} | get -o port | default 0))
|
|
let image = ($comp | get -o image | default "")
|
|
{
|
|
component: $comp_name,
|
|
mode: $mode_raw,
|
|
port: $port,
|
|
image: $image,
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
} | where { $in != null }
|
|
}
|
|
|
|
# Show storage topology: available classes from capabilities.ncl and per-component requirements.
|
|
export def "main describe storage" [
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
] : nothing -> record {
|
|
let ws = (oq-resolve-ws $workspace)
|
|
let ws_root = $ws.root
|
|
let infra = (oq-detect-infra $ws_root)
|
|
let prov_root = (oq-prov-root)
|
|
|
|
# Available storage classes from capabilities.ncl.
|
|
let caps_path = ($ws_root | path join "infra" $infra "capabilities.ncl")
|
|
let available_classes = if ($caps_path | path exists) {
|
|
ncl-eval-soft $caps_path [$ws_root $prov_root] {} | get -o provides | default {} | get -o storage_classes | default []
|
|
} else { [] }
|
|
|
|
# Per-component storage requirements.
|
|
let components = (oq-load-components $ws_root)
|
|
let component_requirements = ($components | columns | each {|comp_name|
|
|
let comp = ($components | get $comp_name)
|
|
let requires = ($comp | get -o requires | default {})
|
|
let storage = ($requires | get -o storage | default null)
|
|
if $storage != null {
|
|
{
|
|
component: $comp_name,
|
|
size: ($storage | get -o size | default ""),
|
|
storage_class: ($storage | get -o storage_class | default ($comp | get -o storage_class | default "")),
|
|
persistent: ($storage | get -o persistent | default false),
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
} | where { $in != null })
|
|
|
|
{
|
|
available_classes: $available_classes,
|
|
component_requirements: $component_requirements,
|
|
}
|
|
}
|
|
|
|
# Show a full workflow definition with FSM state and backlog references.
|
|
#
|
|
# Finds the workflow by id across all infra/*/workflows/*.ncl files and returns
|
|
# its steps, FSM dimension state, and any backlog_refs declared in metadata.
|
|
export def "main describe workflow" [
|
|
workflow_id: string # Workflow id to describe
|
|
--workspace (-w): string # Workspace name (default: active)
|
|
] : nothing -> record {
|
|
let ws = (oq-resolve-ws $workspace)
|
|
let ws_root = $ws.root
|
|
let prov_root = (oq-prov-root)
|
|
|
|
let wf_files = (oq-collect-workflows $ws_root)
|
|
if ($wf_files | is-empty) {
|
|
error make { msg: $"No workflow files found under ($ws_root)/infra/*/workflows/" }
|
|
}
|
|
|
|
mut wf_def = null
|
|
mut wf_meta = null
|
|
|
|
for wf_file in $wf_files {
|
|
let exported = (oq-ncl-export $ws_root $wf_file)
|
|
for key in ($exported | columns) {
|
|
let entry = ($exported | get $key)
|
|
if ($key | str ends-with "metadata") {
|
|
if ($entry | get -o id | default "") == $workflow_id {
|
|
$wf_meta = $entry
|
|
}
|
|
} else {
|
|
if ($entry | get -o id | default "") == $workflow_id {
|
|
$wf_def = $entry
|
|
}
|
|
}
|
|
}
|
|
if $wf_def != null { break }
|
|
}
|
|
|
|
if $wf_def == null {
|
|
error make { msg: $"Workflow '($workflow_id)' not found in any infra/*/workflows/*.ncl under ($ws_root)" }
|
|
}
|
|
|
|
# FSM dimension state.
|
|
let fsm_dim = if $wf_meta != null {
|
|
$wf_meta | get -o fsm_dimension | default ""
|
|
} else { "" }
|
|
|
|
let fsm_state = if ($fsm_dim | is-not-empty) {
|
|
let state_path = ($ws_root | path join ".ontology" "state.ncl")
|
|
if ($state_path | path exists) {
|
|
let state_data2 = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) null)
|
|
if ($state_data2 | is-not-empty) {
|
|
let dims = ($state_data2 | get -o dimensions | default [])
|
|
let dim = ($dims | where {|d| $d.id == $fsm_dim})
|
|
if ($dim | is-empty) {
|
|
{ dimension: $fsm_dim, current_state: "unknown", desired_state: "unknown" }
|
|
} else {
|
|
let d = ($dim | first)
|
|
{
|
|
dimension: $fsm_dim,
|
|
current_state: ($d | get -o current_state | default "unknown"),
|
|
desired_state: ($d | get -o desired_state | default "unknown"),
|
|
}
|
|
}
|
|
} else {
|
|
{ dimension: $fsm_dim, current_state: "unknown", desired_state: "unknown" }
|
|
}
|
|
} else {
|
|
{ dimension: $fsm_dim, current_state: "unknown", desired_state: "unknown" }
|
|
}
|
|
} else {
|
|
{ dimension: null, current_state: "unknown", desired_state: "unknown" }
|
|
}
|
|
|
|
{
|
|
id: $workflow_id,
|
|
name: (if $wf_meta != null { $wf_meta | get -o name | default $workflow_id } else { $workflow_id }),
|
|
description: (if $wf_meta != null { $wf_meta | get -o description | default "" } else { $wf_def | get -o description | default "" }),
|
|
steps: ($wf_def | get -o steps | default []),
|
|
fsm_state: $fsm_state,
|
|
backlog_refs: (if $wf_meta != null { $wf_meta | get -o backlog_refs | default [] } else { [] }),
|
|
}
|
|
}
|