prvng_core/nulib/provisioning
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

492 lines
20 KiB
Text
Executable file

#!/usr/bin/env nu
# Info: Script to run Provisioning
# Author: Jesus Perez Lorenzo
# Release: 2.0.4
# Date: 6-2-2024
# CRITICAL: Must be in export-env block so it runs DURING PARSING,
# not after. This sets up NU_LIB_DIRS before modules are loaded.
export-env {
# Initialize NU_LIB_DIRS, handling both string (from bash) and list (from Nushell)
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
}
# Ensure known provisioning paths are in NU_LIB_DIRS
let default_paths = [
"/opt/provisioning/core/nulib"
"/usr/local/provisioning/core/nulib"
]
# Combine paths: use default paths first, then add any from current
$env.NU_LIB_DIRS = ($default_paths | append $current_lib_dirs)
# Auto-load tera plugin BEFORE loading any modules
# This ensures tera-render is available throughout the script
if ( (version).installed_plugins | str contains "tera" ) {
(plugin use tera)
}
}
use std log
use lib_provisioning *
use env.nu *
#Load all main defs
use main_provisioning *
#module srv { use instances.nu * }
use servers/ssh.nu *
use servers/utils.nu *
use taskservs/utils.nu find_taskserv
# Bootstrap will be loaded on-demand only when needed for real operations
# use lib_provisioning/platform/bootstrap.nu *
# Helper: Reorder arguments to put flags before positional args
# This allows: provisioning workspace update --yes
# Instead of requiring: provisioning --yes workspace update
# NOTE: Nushell's parameter parsing handles interleaved flags well, so we just return args as-is
# This avoids breaking flag:value pairs
def reorder_args [args: list]: nothing -> list {
$args
}
# Help on provisioning commands
export def "main help" [
...args: string # Optional category: infrastructure, orchestration, development, workspace, concepts
--notitles # not titles
--out: string # Print Output format: json, yaml, text (default)
] {
if $notitles == null or not $notitles { show_titles }
if ($out | is-not-empty) { $env.PROVISIONING_NO_TERMINAL = false }
# Use only the first argument, ignore any extras (e.g., "orch status" -> "orch")
let category = if ($args | length) > 0 { ($args | get 0) } else { "" }
print (provisioning_options $category)
if not $env.PROVISIONING_DEBUG { end_run "" }
}
def main [
...args: string # Other options, use help to get info
--infra (-i): string # Cloud directory
--settings (-s): string # Settings path
--serverpos (-p): int # Server position in settings
--outfile (-o): string # Output file
--template(-t): string # Template path or name in PROVISION_KLOUDS_PATH
--check (-c) # Only check mode no servers will be created
--upload (-u) # Upload scripts to server for inspection without executing (use with --check)
--yes (-y) # confirm task
--wait # Wait servers to be created
--keepstorage # keep storage
--select: string # Select with task as option
--onsel: string # On selection: e (edit) | v (view) | l (list) | t (tree)
--infras: string # Infra list names separated by commas
--new (-n): string # New infrastructure name
--debug (-x) # Use Debug mode
--xm # Debug with PROVISIONING_METADATA
--xc # Debug for task and services locally PROVISIONING_DEBUG_CHECK
--xr # Debug for remote servers PROVISIONING_DEBUG_REMOTE
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
--nc # Not clean working settings
--metadata # Error with metadata (-xm)
--notitles # not tittles
--environment: string # Environment override (dev/test/prod)
--dep-option: string # Workspace dependency option: workspace-home, home-package, git-package, publish-repo
--dep-url: string # Dependency URL for git-package or publish-repo
--dry-run # Show what would be done without doing it (pack command)
--force (-f) # Skip confirmation prompts (pack/delete commands)
--all # Process all items (pack clean command)
--keep-latest: int # Keep N latest versions (pack clean command)
--workspace (-w): string # Workspace name (for bootstrap, cluster deploy, etc.)
--activate # Activate workspace as default (workspace commands)
--interactive # Interactive workspace creation wizard
--org: string # Organization name (for detect/complete commands)
--apply # Apply changes (for complete command)
--verbose # Verbose output (for detect/complete/workflow commands)
--pretty # Pretty-print JSON/YAML output (for detect/complete commands)
-v # Show version
--version (-V) # Show version with title
--info # Show Info with title
--about # Show About
--helpinfo (-h) # For more details use options "help" (no dashes)
--out: string # Print Output format: json, yaml, text (default)
--view # Print with highlight
--inputfile: string # Input format: json, yaml, text (default)
--include_notuse # Include servers not use
--services: string # Platform services set: core, all, custom (for platform start)
]: nothing -> nothing {
# Reorder arguments: move flags to the beginning
# This allows: provisioning workspace update --yes
let reordered_args = (reorder_args $args)
# Extract flags from reordered args (for flags that came after positional args)
let has_yes_in_args = ($reordered_args | any {|x| $x == "--yes" or $x == "-y"})
let has_check_in_args = ($reordered_args | any {|x| $x == "--check" or $x == "-c"})
let has_upload_in_args = ($reordered_args | any {|x| $x == "--upload" or $x == "-u"})
let has_force_in_args = ($reordered_args | any {|x| $x == "--force" or $x == "-f"})
let has_verbose_in_args = ($reordered_args | any {|x| $x == "--verbose" or $x == "-v"})
let has_wait_in_args = ($reordered_args | any {|x| $x == "--wait"})
# Combine with already-parsed flags (take OR - if either parsed or in args, then true)
let final_yes = ($yes or $has_yes_in_args)
let final_check = ($check or $has_check_in_args)
let final_upload = ($upload or $has_upload_in_args)
let final_force = ($force or $has_force_in_args)
let final_verbose = ($verbose or $has_verbose_in_args)
let final_wait = ($wait or $has_wait_in_args)
# Initialize provisioning system
provisioning_init $helpinfo "" $reordered_args
# Parse all flags into normalized structure
let parsed_flags = (parse_common_flags {
version: $version, v: $v, info: $info, about: $about,
debug: $debug, metadata: $metadata, xc: $xc, xr: $xr, xld: $xld,
check: $final_check, upload: $final_upload, yes: $final_yes, wait: $final_wait, keepstorage: $keepstorage,
nc: $nc, include_notuse: $include_notuse,
out: $out, notitles: $notitles, view: $view,
infra: $infra, infras: $infras, settings: $settings, outfile: $outfile,
template: $template, select: $select, onsel: $onsel, serverpos: $serverpos,
new: $new, environment: $environment,
dep_option: $dep_option, dep_url: $dep_url,
dry_run: $dry_run, force: $final_force, all: $all, keep_latest: $keep_latest,
activate: $activate, interactive: $interactive,
org: $org, apply: $apply, verbose: $final_verbose, pretty: $pretty,
services: $services, workspace: $workspace
})
# Handle version, info, about flags
if $parsed_flags.show_version { ^$env.PROVISIONING_NAME -v ; exit }
if $parsed_flags.show_info { ^$env.PROVISIONING_NAME -i ; exit }
if $parsed_flags.show_about { _print (get_about_info) ; exit }
# Bootstrap platform services (only if running actual commands, not help/info)
# Skip bootstrap for help-like, guide, setup, discovery/info, and utility commands
# Updated for Phase 1: Fast-Path Expansion - Include read-only workspace commands
let is_help_command = (
($reordered_args | length) == 0 or
($reordered_args | get 0) in [
# Help and guides
"help", "-h", "--help",
"sc", "shortcuts", "quickstart", "quick",
"from-scratch", "scratch",
"customize", "custom",
"guide", "guides", "howto",
# Setup
"setup", "st",
# Workspace commands (read-only, fast-path)
"workspace", "ws",
# Discovery and module commands
"mod", "module", "discover", "disc",
"dt", "dp", "dc",
"discover-taskservs", "disc-t",
"discover-providers", "disc-p",
"discover-clusters", "disc-c",
# Development info
"lyr", "layer", "version", "pack",
# Utilities and info
"nuinfo", "env", "allenv",
"validate", "val", "show", "config-template",
"cache",
"list", "l", "ls",
"plugin", "plugins",
"qr", "ssh", "sops",
"providers",
# Diagnostics commands (workspace-agnostic)
"status", "health", "diagnostics", "next", "phase"
]
)
# Check if this is a command that doesn't need platform bootstrap
# VM commands and infrastructure commands can work without bootstrap
# Also skip bootstrap if --check flag is present (validation mode, no execution needed)
let skip_bootstrap = (
(($reordered_args | length) > 0 and
($reordered_args | get 0) in [
# Interactive Nushell session (no bootstrap needed)
"nu",
# Platform commands (don't need bootstrap)
"platform", "plat", "p",
# VM commands (info/list only, no bootstrap needed)
"vm", "vmi", "vmh", "vml",
# Infrastructure commands can work offline
"server", "s",
"taskserv", "task", "t",
"cluster", "cl",
"bootstrap",
# Create command (with various targets)
"create", "c",
# Delete command
"delete", "d",
# Update command
"update", "u",
# Build commands (image management, doesn't need orchestrator)
"build", "b", "bi", "build-image"
]) or
# Skip bootstrap if in check mode (validation/dry-run, no execution needed)
$final_check
)
if (not $is_help_command) and (not $skip_bootstrap) {
# Load bootstrap module dynamically when needed
use lib_provisioning/platform/bootstrap.nu *
let bootstrap_result = (bootstrap-platform --auto-start --timeout=60 --verbose=($final_verbose))
if not $bootstrap_result.all_healthy {
_print ""
_print $"(_ansi red)❌ Platform services not healthy(_ansi reset)"
_print ""
_print "Failed services:"
for service in ($bootstrap_result.services | where {|s| $s.status != "healthy"}) {
_print $" - ($service.name): ($service.action)"
}
_print ""
_print "To start services manually:"
_print " cd provisioning/platform && docker-compose up -d"
_print ""
exit 1
}
}
# DEBUG
if ($env.PROVISIONING_DEBUG? | default false) {
print $"DEBUG provisioning: reordered_args = ($reordered_args)" >&2
print $"DEBUG provisioning: parsed_flags.infra = (($parsed_flags | get -o infra | default 'MISSING'))" >&2
}
# Handle help command BEFORE dispatcher to avoid infinite loop
# The dispatcher used to call "exec provisioning help" which created infinite recursion
if (($reordered_args | length) > 0) and (($reordered_args | get 0) in ["help", "h"]) {
if ($env.PROVISIONING_DEBUG? | default false) {
print $"DEBUG: Help command detected, args=($reordered_args)" >&2
}
let category = if ($reordered_args | length) > 1 { ($reordered_args | get 1) } else { "" }
print (provisioning_options $category)
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
return
}
# For info/discovery/utility commands, dispatch directly without going through workspace enforcement
# These commands don't need workspace context
if (($reordered_args | length) > 0) and (($reordered_args | get 0) in [
# Guide commands
"guide", "guides", "sc", "howto", "shortcuts", "quickstart", "quick",
"from-scratch", "scratch", "customize", "custom",
# Discovery/info commands
"mod", "module", "discover", "disc",
"dt", "dp", "dc",
"discover-taskservs", "disc-t",
"discover-providers", "disc-p",
"discover-clusters", "disc-c",
"lyr", "layer", "version",
"nuinfo", "env", "allenv",
"validate", "val", "show", "cache",
# Utility commands (these are informational)
"plugin", "plugins",
"qr", "nuinfo",
# Diagnostics commands (workspace-agnostic)
"status", "health", "diagnostics", "next", "phase"
]) {
dispatch_command $reordered_args $parsed_flags
if not $env.PROVISIONING_DEBUG { end_run "" }
return
}
# Check if we're in module mode (invoked with -mod flag from bash wrapper)
# If so, bypass dispatcher and call the module directly
if ($env.PROVISIONING_MODULE? | default "" | is-not-empty) {
let module = $env.PROVISIONING_MODULE
# At this point, $reordered_args contains [create, ...] or whatever the user provided after -mod
# We need to invoke the module's main function
match $module {
"server" => {
use servers/create.nu *
# Ensure tera plugin is loaded for template rendering
let tera_available = ((plugin list | where name == "tera" | length) > 0)
if $tera_available {
if ($env.PROVISIONING_DEBUG? | default false) {
_print "DEBUG: Loading tera plugin (-mod server)..." >&2
}
(plugin use tera)
if ($env.PROVISIONING_DEBUG? | default false) {
_print "DEBUG: Tera plugin loaded for -mod server" >&2
}
}
# Call server create module main function
# $reordered_args now has ["create"] or ["delete"] or ["list"] etc.
main ...$reordered_args --check=$final_check --wait=$final_wait --infra=($infra | default "") --settings=($settings | default "") --outfile=($outfile | default "") --debug=$debug --xm=$xm --xc=$xc --xr=$xr --xld=$xld --metadata=$metadata --notitles=$notitles --out=($out | default "")
}
"taskserv" | "task" => {
use taskservs/create.nu *
main ...$reordered_args --check=$final_check --upload=$final_upload --wait=$final_wait --debug=$debug
}
"cluster" => {
use clusters/create.nu *
main ...$reordered_args --check=$final_check --debug=$debug
}
"images" => {
use images/create.nu *
use images/list.nu *
use images/update.nu *
use images/delete.nu *
use images/state.nu *
use images/watch.nu *
# $reordered_args now has ["create", "cp", "--infra", "..."] or similar
let subcommand = if ($reordered_args | length) > 0 { $reordered_args | get 0 } else { "help" }
match $subcommand {
"create" | "c" => {
let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" }
let infra_arg = if ($infra | is-not-empty) { $infra } else { "" }
image-create $role --infra=$infra_arg --check=$final_check
}
"list" | "l" => {
let provider = if ($infra | is-not-empty) { $infra } else { "" }
image-list --provider=$provider
}
"update" | "u" => {
let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" }
let infra_arg = if ($infra | is-not-empty) { $infra } else { "" }
image-update $role --infra=$infra_arg --check=$final_check
}
"delete" | "d" => {
let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" }
image-delete $role --yes=$final_yes
}
"state" | "s" => {
image-state-list --provider=$infra
}
"watch" | "w" => {
let interval = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "30" }
image-watch --interval=($interval | into int)
}
"help" | "h" | _ => {
print "Image Management Commands"
print "======================="
print ""
print "Usage: provisioning build image <command> [options]"
print ""
print "Commands:"
print " create <role> - Build snapshot for role"
print " list - Show all role states"
print " update <role> - Rebuild stale snapshot"
print " delete <role> - Remove snapshot + state"
print " state - List all state files"
print " watch - Monitor role freshness"
print ""
print "Options:"
print " --infra <path> - Infrastructure directory"
print " --check - Validate without executing"
print " --yes - Skip confirmation"
print ""
}
}
}
_ => {
print $"Unknown module: ($module)"
exit 1
}
}
} else {
# Normal command dispatch through dispatcher
dispatch_command $reordered_args $parsed_flags
}
# End run if not in debug mode
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
}
export def get_show_info [
ops: list
curr_settings: record
out: string
]: nothing -> record {
match ($ops | get -o 0 | default "") {
"set" |"setting" | "settings" => $curr_settings,
"def" | "defs" |"defsetting" | "defsettings" => {
let src = ($curr_settings | get -o src | default "");
let src_path = ($curr_settings | get -o src_path | default "");
let def_settings = if ($src_path | path join $src | path exists) {
open -r ($src_path | path join $src)
} else { "" }
let main_path = ($env.PROVISIONING | path join "kcl" | path join "settings.k")
let src_main_settings = if ($main_path | path exists) {
open -r $main_path
} else { "" }
{
def: $src,
def_path: $src_path,
infra: ($curr_settings | get -o infra | default ""),
infra_path: ($curr_settings | get -o infra_path | default ""),
def_settings: $def_settings,
main_path: $main_path,
main_settings: $src_main_settings,
}
},
"server" |"servers" | "s" => {
let servers = ($curr_settings | get -o data | get -o servers | default {})
let item = ($ops | get -o 1 | default "")
if ($item | is-empty) {
$servers
} else {
let server = (find_server $item $servers ($out | default ""))
let def_target = ($ops | get -o 2 | default "")
match $def_target {
"t" | "task" | "taskserv" => {
let task = ($ops | get -o 3 | default "")
(find_taskserv $curr_settings $server $task ($out | default ""))
},
_ => $server,
}
}
},
"serverdefs" |"serversdefs" | "sd" => {
(find_serversdefs $curr_settings)
},
"provgendefs" |"provgendef" | "pgd" => {
(find_provgendefs)
},
"taskservs" |"taskservs" | "ts" => {
#(list_taskservs $curr_settings)
let list_taskservs = (taskservs_list)
if ($list_taskservs | length) == 0 {
_print $"🛑 no items found for (_ansi cyan)taskservs list(_ansi reset)"
return
}
$list_taskservs
},
"taskservsgendefs" |"taskservsgendef" | "tsd" => {
let defs_path = ($env.PROVISIONING_TASKSERVS_PATH | path join $env.PROVISIONING_GENERATE_DIRPATH | path join $env.PROVISIONING_GENERATE_DEFSFILE)
if ($defs_path | path exists) {
open $defs_path
}
},
"cost" | "costs" | "c" | "price" | "prices" | "p" => {
(servers_walk_by_costs $curr_settings "" false false "stdout")
},
"alldata" => ($curr_settings | get -o data | default {}
| merge { costs: (servers_walk_by_costs $curr_settings "" false false "stdout") }
),
"data" | _ => {
if ($out | is-not-empty) {
($curr_settings | get -o data | default {})
} else {
print ($" (_ansi cyan_bold)($curr_settings | get -o data | get -o main_name | default '')"
+ $"(_ansi reset): (_ansi yellow_bold)($curr_settings | get -o data | get -o main_title | default '') (_ansi reset)"
)
print ($curr_settings | get -o data | default {} | merge { servers: ''})
($curr_settings | get -o data | default {} | get -o servers | each {|item|
print $"\n server: (_ansi cyan_bold)($item.hostname | default '') (_ansi reset)"
print $item
})
""
}
},
}
}