core: clean-start baseline — engine + core_catalog (constellation materialization)
This commit is contained in:
parent
8affca20aa
commit
834a5a8632
500 changed files with 122090 additions and 0 deletions
37
.githooks/pre-commit
Executable file
37
.githooks/pre-commit
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
use toolkit.nu fmt
|
||||
|
||||
fmt # --check --verbose
|
||||
|
||||
# ADR-025: Block root star-imports of lib_provisioning / main_provisioning.
|
||||
# A line matching `use lib_provisioning *` or `use main_provisioning *` at the
|
||||
# start of a file (top-level) reintroduces the transitive parse cost this
|
||||
# refactor was designed to eliminate. All imports must be selective.
|
||||
let staged = (git diff --cached --name-only | lines | where { str ends-with ".nu" })
|
||||
|
||||
if ($staged | length) > 0 {
|
||||
let violations = (
|
||||
$staged
|
||||
| each {|f|
|
||||
let hits = (
|
||||
do { git show $":($f)" } | complete
|
||||
| if $in.exit_code == 0 { $in.stdout } else { "" }
|
||||
| lines
|
||||
| enumerate
|
||||
| where { $it.item | str starts-with "use lib_provisioning *" or $it.item | str starts-with "use main_provisioning *" }
|
||||
| each {|row| $" ($f):($row.index + 1): ($row.item | str trim)"}
|
||||
)
|
||||
$hits
|
||||
}
|
||||
| flatten
|
||||
)
|
||||
|
||||
if ($violations | length) > 0 {
|
||||
print "❌ ADR-025 star-import violation — selective imports required:"
|
||||
for v in $violations { print $v }
|
||||
print ""
|
||||
print "Replace `use lib_provisioning *` with explicit `use lib_provisioning/path/to/module.nu [sym1 sym2]`"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
6
.githooks/pre-push
Executable file
6
.githooks/pre-push
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
use toolkit.nu [fmt, clippy]
|
||||
|
||||
fmt --check --verbose
|
||||
#clippy --verbose
|
||||
674
.githooks/toolkit.nu
Normal file
674
.githooks/toolkit.nu
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
# this module regroups a bunch of development tools to make the development
|
||||
# process easier for anyone.
|
||||
#
|
||||
# the main purpose of `toolkit` is to offer an easy to use interface for the
|
||||
# developer during a PR cycle, namely to (**1**) format the source base,
|
||||
# (**2**) catch classical flaws in the new changes with *clippy* and (**3**)
|
||||
# make sure all the tests pass.
|
||||
|
||||
const toolkit_dir = path self .
|
||||
|
||||
# check standard code formatting and apply the changes
|
||||
export def fmt [
|
||||
--check # do not apply the format changes, only check the syntax
|
||||
--verbose # print extra information about the command's progress
|
||||
] {
|
||||
if $verbose {
|
||||
print $"running ('toolkit fmt' | pretty-format-command)"
|
||||
}
|
||||
|
||||
if $check {
|
||||
let result = (do { ^cargo fmt --all -- --check } | complete)
|
||||
if $result.exit_code != 0 {
|
||||
error make --unspanned {
|
||||
msg: $"\nplease run ('toolkit fmt' | pretty-format-command) to fix formatting!"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
^cargo fmt --all
|
||||
}
|
||||
}
|
||||
|
||||
# check that you're using the standard code style
|
||||
#
|
||||
# > it is important to make `clippy` happy :relieved:
|
||||
export def clippy [
|
||||
--verbose # print extra information about the command's progress
|
||||
--features: list<string> # the list of features to run *Clippy* on
|
||||
] {
|
||||
if $verbose {
|
||||
print $"running ('toolkit clippy' | pretty-format-command)"
|
||||
}
|
||||
|
||||
# If changing these settings also change CI settings in .github/workflows/ci.yml
|
||||
let result1 = (do {
|
||||
^cargo clippy
|
||||
--workspace
|
||||
--exclude nu_plugin_*
|
||||
--features ($features | default [] | str join ",")
|
||||
--
|
||||
-D warnings
|
||||
-D clippy::unwrap_used
|
||||
-D clippy::unchecked_duration_subtraction
|
||||
} | complete)
|
||||
|
||||
if $result1.exit_code != 0 {
|
||||
error make --unspanned {
|
||||
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
|
||||
}
|
||||
}
|
||||
|
||||
if $verbose {
|
||||
print $"running ('toolkit clippy' | pretty-format-command) on tests"
|
||||
}
|
||||
# In tests we don't have to deny unwrap
|
||||
let result2 = (do {
|
||||
^cargo clippy
|
||||
--tests
|
||||
--workspace
|
||||
--exclude nu_plugin_*
|
||||
--features ($features | default [] | str join ",")
|
||||
--
|
||||
-D warnings
|
||||
} | complete)
|
||||
|
||||
if $result2.exit_code != 0 {
|
||||
error make --unspanned {
|
||||
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
|
||||
}
|
||||
}
|
||||
|
||||
if $verbose {
|
||||
print $"running ('toolkit clippy' | pretty-format-command) on plugins"
|
||||
}
|
||||
let result3 = (do {
|
||||
^cargo clippy
|
||||
--package nu_plugin_*
|
||||
--
|
||||
-D warnings
|
||||
-D clippy::unwrap_used
|
||||
-D clippy::unchecked_duration_subtraction
|
||||
} | complete)
|
||||
|
||||
if $result3.exit_code != 0 {
|
||||
error make --unspanned {
|
||||
msg: $"\nplease fix the above ('clippy' | pretty-format-command) errors before continuing!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# check that all the tests pass
|
||||
export def test [
|
||||
--fast # use the "nextext" `cargo` subcommand to speed up the tests (see [`cargo-nextest`](https://nexte.st/) and [`nextest-rs/nextest`](https://github.com/nextest-rs/nextest))
|
||||
--features: list<string> # the list of features to run the tests on
|
||||
--workspace # run the *Clippy* command on the whole workspace (overrides `--features`)
|
||||
] {
|
||||
if $fast {
|
||||
if $workspace {
|
||||
^cargo nextest run --all
|
||||
} else {
|
||||
^cargo nextest run --features ($features | default [] | str join ",")
|
||||
}
|
||||
} else {
|
||||
if $workspace {
|
||||
^cargo test --workspace
|
||||
} else {
|
||||
^cargo test --features ($features | default [] | str join ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# run the tests for the standard library
|
||||
export def "test stdlib" [
|
||||
--extra-args: string = ''
|
||||
] {
|
||||
^cargo run -- --no-config-file -c $"
|
||||
use crates/nu-std/testing.nu
|
||||
testing run-tests --path crates/nu-std ($extra_args)
|
||||
"
|
||||
}
|
||||
|
||||
# formats the pipe input inside backticks, dimmed and italic, as a pretty command
|
||||
def pretty-format-command [] {
|
||||
$"`(ansi default_dimmed)(ansi default_italic)($in)(ansi reset)`"
|
||||
}
|
||||
|
||||
# return a report about the check stage
|
||||
#
|
||||
# - fmt comes first
|
||||
# - then clippy
|
||||
# - and finally the tests
|
||||
#
|
||||
# without any option, `report` will return an empty report.
|
||||
# otherwise, the truth values will be incremental, following
|
||||
# the order above.
|
||||
def report [
|
||||
--fail-fmt
|
||||
--fail-clippy
|
||||
--fail-test
|
||||
--fail-test-stdlib
|
||||
--no-fail
|
||||
] {
|
||||
[fmt clippy test "test stdlib"]
|
||||
| wrap stage
|
||||
| merge (
|
||||
if $no_fail { [true true true true] }
|
||||
else if $fail_fmt { [false null null null] }
|
||||
else if $fail_clippy { [true false null null] }
|
||||
else if $fail_test { [true true false null] }
|
||||
else if $fail_test_stdlib { [true true true false] }
|
||||
else { [null null null null] }
|
||||
| wrap success
|
||||
)
|
||||
| upsert emoji {|it|
|
||||
if ($it.success == null) {
|
||||
":black_circle:"
|
||||
} else if $it.success {
|
||||
":green_circle:"
|
||||
} else {
|
||||
":red_circle:"
|
||||
}
|
||||
}
|
||||
| each {|it|
|
||||
$"- ($it.emoji) `toolkit ($it.stage)`"
|
||||
}
|
||||
| to text
|
||||
}
|
||||
|
||||
# run all the necessary checks and tests to submit a perfect PR
|
||||
#
|
||||
# # Example
|
||||
# let us say we apply a change that
|
||||
# - breaks the formatting, e.g. with extra newlines everywhere
|
||||
# - makes clippy sad, e.g. by adding unnecessary string conversions with `.to_string()`
|
||||
# - breaks the tests by output bad string data from a data structure conversion
|
||||
#
|
||||
# > the following diff breaks all of the three checks!
|
||||
# > ```diff
|
||||
# > diff --git a/crates/nu-command/src/formats/to/nuon.rs b/crates/nu-command/src/formats/to/nuon.rs
|
||||
# > index abe34c054..927d6a3de 100644
|
||||
# > --- a/crates/nu-command/src/formats/to/nuon.rs
|
||||
# > +++ b/crates/nu-command/src/formats/to/nuon.rs
|
||||
# > @@ -131,7 +131,8 @@ pub fn value_to_string(v: &Value, span: Span) -> Result<String, ShellError> {
|
||||
# > }
|
||||
# > })
|
||||
# > .collect();
|
||||
# > - let headers_output = headers.join(", ");
|
||||
# > + let headers_output = headers.join(&format!("x {}", "")
|
||||
# > + .to_string());
|
||||
# >
|
||||
# > let mut table_output = vec![];
|
||||
# > for val in vals {
|
||||
# > ```
|
||||
#
|
||||
# > **Note**
|
||||
# > at every stage, the `toolkit check pr` will return a report of the few stages being run.
|
||||
#
|
||||
# - we run the toolkit once and it fails...
|
||||
# ```nushell
|
||||
# >_ toolkit check pr
|
||||
# running `toolkit fmt`
|
||||
# Diff in /home/amtoine/.local/share/git/store/github.com/amtoine/nushell/crates/nu-command/src/formats/to/nuon.rs at line 131:
|
||||
# }
|
||||
# })
|
||||
# .collect();
|
||||
# - let headers_output = headers.join(&format!("x {}", "")
|
||||
# - .to_string());
|
||||
# + let headers_output = headers.join(&format!("x {}", "").to_string());
|
||||
#
|
||||
# let mut table_output = vec![];
|
||||
# for val in vals {
|
||||
#
|
||||
# please run toolkit fmt to fix the formatting
|
||||
# ```
|
||||
# - we run `toolkit fmt` as proposed and rerun the toolkit... to see clippy is sad...
|
||||
# ```nushell
|
||||
# running `toolkit fmt`
|
||||
# running `toolkit clippy`
|
||||
# ...
|
||||
# error: redundant clone
|
||||
# --> crates/nu-command/src/formats/to/nuon.rs:134:71
|
||||
# |
|
||||
# 134 | let headers_output = headers.join(&format!("x {}", "").to_string());
|
||||
# | ^^^^^^^^^^^^ help: remove this
|
||||
# |
|
||||
# note: this value is dropped without further use
|
||||
# --> crates/nu-command/src/formats/to/nuon.rs:134:52
|
||||
# |
|
||||
# 134 | let headers_output = headers.join(&format!("x {}", "").to_string());
|
||||
# | ^^^^^^^^^^^^^^^^^^^
|
||||
# = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone
|
||||
# = note: `-D clippy::redundant-clone` implied by `-D warnings`
|
||||
#
|
||||
# error: could not compile `nu-command` due to previous error
|
||||
# ```
|
||||
# - we remove the useless `.to_string()`, and in that cases, the whole format is useless, only `"x "` is useful!
|
||||
# but now the tests do not pass :sob:
|
||||
# ```nushell
|
||||
# running `toolkit fmt`
|
||||
# running `toolkit clippy`
|
||||
# ...
|
||||
# running `toolkit test`
|
||||
# ...
|
||||
# failures:
|
||||
# commands::insert::insert_uses_enumerate_index
|
||||
# commands::merge::multi_row_table_overwrite
|
||||
# commands::merge::single_row_table_no_overwrite
|
||||
# commands::merge::single_row_table_overwrite
|
||||
# commands::update::update_uses_enumerate_index
|
||||
# commands::upsert::upsert_uses_enumerate_index_inserting
|
||||
# commands::upsert::upsert_uses_enumerate_index_updating
|
||||
# commands::where_::where_uses_enumerate_index
|
||||
# format_conversions::nuon::does_not_quote_strings_unnecessarily
|
||||
# format_conversions::nuon::to_nuon_table
|
||||
# ```
|
||||
# - finally let's fix the tests by removing the `x`, essentially removing the whole diff we applied at the top!
|
||||
#
|
||||
# now the whole `toolkit check pr` passes! :tada:
|
||||
export def "check pr" [
|
||||
--fast # use the "nextext" `cargo` subcommand to speed up the tests (see [`cargo-nextest`](https://nexte.st/) and [`nextest-rs/nextest`](https://github.com/nextest-rs/nextest))
|
||||
--features: list<string> # the list of features to check the current PR on
|
||||
] {
|
||||
$env.NU_TEST_LOCALE_OVERRIDE = 'en_US.utf8'
|
||||
$env.LANG = 'en_US.UTF-8'
|
||||
$env.LANGUAGE = 'en'
|
||||
|
||||
let fmt_result = (do { fmt --check --verbose } | complete)
|
||||
if $fmt_result.exit_code != 0 {
|
||||
return (report --fail-fmt)
|
||||
}
|
||||
|
||||
let clippy_result = (do { clippy --features $features --verbose } | complete)
|
||||
if $clippy_result.exit_code != 0 {
|
||||
return (report --fail-clippy)
|
||||
}
|
||||
|
||||
print $"running ('toolkit test' | pretty-format-command)"
|
||||
let test_result = (do {
|
||||
if $fast {
|
||||
if ($features | is-empty) {
|
||||
test --workspace --fast
|
||||
} else {
|
||||
test --features $features --fast
|
||||
}
|
||||
} else {
|
||||
if ($features | is-empty) {
|
||||
test --workspace
|
||||
} else {
|
||||
test --features $features
|
||||
}
|
||||
}
|
||||
} | complete)
|
||||
|
||||
if $test_result.exit_code != 0 {
|
||||
return (report --fail-test)
|
||||
}
|
||||
|
||||
print $"running ('toolkit test stdlib' | pretty-format-command)"
|
||||
let stdlib_result = (do { test stdlib } | complete)
|
||||
if $stdlib_result.exit_code != 0 {
|
||||
return (report --fail-test-stdlib)
|
||||
}
|
||||
|
||||
report --no-fail
|
||||
}
|
||||
|
||||
# run Nushell from source with a right indicator
|
||||
export def run [] {
|
||||
^cargo run -- ...[
|
||||
-e "$env.PROMPT_COMMAND_RIGHT = $'(ansi magenta_reverse)trying Nushell inside Cargo(ansi reset)'"
|
||||
]
|
||||
}
|
||||
|
||||
# set up git hooks to run:
|
||||
# - `toolkit fmt --check --verbose` on `git commit`
|
||||
# - `toolkit fmt --check --verbose` and `toolkit clippy --verbose` on `git push`
|
||||
export def setup-git-hooks [] {
|
||||
print "This command will change your local git configuration and hence modify your development workflow. Are you sure you want to continue? [y]"
|
||||
if (input) == "y" {
|
||||
print $"running ('toolkit setup-git-hooks' | pretty-format-command)"
|
||||
git config --local core.hooksPath .githooks
|
||||
} else {
|
||||
print $"aborting ('toolkit setup-git-hooks' | pretty-format-command)"
|
||||
}
|
||||
}
|
||||
|
||||
def build-nushell [features: string] {
|
||||
print $'(char nl)Building nushell'
|
||||
print '----------------------------'
|
||||
|
||||
^cargo build --features $features --locked
|
||||
}
|
||||
|
||||
def build-plugin [] {
|
||||
let plugin = $in
|
||||
|
||||
print $'(char nl)Building ($plugin)'
|
||||
print '----------------------------'
|
||||
|
||||
cd $"crates/($plugin)"
|
||||
^cargo build
|
||||
}
|
||||
|
||||
# build Nushell and plugins with some features
|
||||
export def build [
|
||||
...features: string@"nu-complete list features" # a space-separated list of feature to install with Nushell
|
||||
--all # build all plugins with Nushell
|
||||
] {
|
||||
build-nushell ($features | default [] | str join ",")
|
||||
|
||||
if not $all {
|
||||
return
|
||||
}
|
||||
|
||||
let plugins = [
|
||||
nu_plugin_inc,
|
||||
nu_plugin_gstat,
|
||||
nu_plugin_query,
|
||||
nu_plugin_polars,
|
||||
nu_plugin_example,
|
||||
nu_plugin_custom_values,
|
||||
nu_plugin_formats,
|
||||
]
|
||||
|
||||
for plugin in $plugins {
|
||||
$plugin | build-plugin
|
||||
}
|
||||
}
|
||||
|
||||
def "nu-complete list features" [] {
|
||||
open Cargo.toml | get features | transpose feature dependencies | get feature
|
||||
}
|
||||
|
||||
def install-plugin [] {
|
||||
let plugin = $in
|
||||
|
||||
print $'(char nl)Installing ($plugin)'
|
||||
print '----------------------------'
|
||||
|
||||
^cargo install --path $"crates/($plugin)"
|
||||
}
|
||||
|
||||
# install Nushell and features you want
|
||||
export def install [
|
||||
...features: string@"nu-complete list features" # a space-separated list of feature to install with Nushell
|
||||
--all # install all plugins with Nushell
|
||||
] {
|
||||
touch crates/nu-cmd-lang/build.rs # needed to make sure `version` has the correct `commit_hash`
|
||||
^cargo install --path . --features ($features | default [] | str join ",") --locked --force
|
||||
if not $all {
|
||||
return
|
||||
}
|
||||
|
||||
let plugins = [
|
||||
nu_plugin_inc,
|
||||
nu_plugin_gstat,
|
||||
nu_plugin_query,
|
||||
nu_plugin_polars,
|
||||
nu_plugin_example,
|
||||
nu_plugin_custom_values,
|
||||
nu_plugin_formats,
|
||||
]
|
||||
|
||||
for plugin in $plugins {
|
||||
$plugin | install-plugin
|
||||
}
|
||||
}
|
||||
|
||||
def windows? [] {
|
||||
$nu.os-info.name == windows
|
||||
}
|
||||
|
||||
# filter out files that end in .d
|
||||
def keep-plugin-executables [] {
|
||||
if (windows?) { where name ends-with '.exe' } else { where name !~ '\.d' }
|
||||
}
|
||||
|
||||
# add all installed plugins
|
||||
export def "add plugins" [] {
|
||||
let plugin_path = (which nu | get path.0 | path dirname)
|
||||
let plugins = (ls $plugin_path | where name =~ nu_plugin | keep-plugin-executables | get name)
|
||||
|
||||
if ($plugins | is-empty) {
|
||||
print $"no plugins found in ($plugin_path)..."
|
||||
return
|
||||
}
|
||||
|
||||
for plugin in $plugins {
|
||||
let plugin_result = (do {
|
||||
print $"> plugin add ($plugin)"
|
||||
plugin add $plugin
|
||||
} | complete)
|
||||
if $plugin_result.exit_code != 0 {
|
||||
print -e $"(ansi rb)Failed to add ($plugin):\n($plugin_result.stderr)(ansi reset)"
|
||||
}
|
||||
}
|
||||
|
||||
print $"\n(ansi gb)plugins registered, please restart nushell(ansi reset)"
|
||||
}
|
||||
|
||||
def compute-coverage [] {
|
||||
print "Setting up environment variables for coverage"
|
||||
# Enable LLVM coverage tracking through environment variables
|
||||
# show env outputs .ini/.toml style description of the variables
|
||||
# In order to use from toml, we need to make sure our string literals are single quoted
|
||||
# This is especially important when running on Windows since "C:\blah" is treated as an escape
|
||||
^cargo llvm-cov show-env | str replace (char dq) (char sq) -a | from toml | load-env
|
||||
|
||||
print "Cleaning up coverage data"
|
||||
^cargo llvm-cov clean --workspace
|
||||
|
||||
print "Building with workspace and profile=ci"
|
||||
# Apparently we need to explicitly build the necessary parts
|
||||
# using the `--profile=ci` is basically `debug` build with unnecessary symbols stripped
|
||||
# leads to smaller binaries and potential savings when compiling and running
|
||||
^cargo build --workspace --profile=ci
|
||||
|
||||
print "Running tests with --workspace and profile=ci"
|
||||
^cargo test --workspace --profile=ci
|
||||
|
||||
# You need to provide the used profile to find the raw data
|
||||
print "Generating coverage report as lcov.info"
|
||||
^cargo llvm-cov report --lcov --output-path lcov.info --profile=ci
|
||||
}
|
||||
|
||||
# Script to generate coverage locally
|
||||
#
|
||||
# Output: `lcov.info` file
|
||||
#
|
||||
# Relies on `cargo-llvm-cov`. Install via `cargo install cargo-llvm-cov`
|
||||
# https://github.com/taiki-e/cargo-llvm-cov
|
||||
#
|
||||
# You probably have to run `cargo llvm-cov clean` once manually,
|
||||
# as you have to confirm to install additional tooling for your rustup toolchain.
|
||||
# Else the script might stall waiting for your `y<ENTER>`
|
||||
#
|
||||
# Some of the internal tests rely on the exact cargo profile
|
||||
# (This is somewhat criminal itself)
|
||||
# but we have to signal to the tests that we use the `ci` `--profile`
|
||||
#
|
||||
# Manual gathering of coverage to catch invocation of the `nu` binary.
|
||||
# This is relevant for tests using the `nu!` macro from `nu-test-support`
|
||||
# see: https://github.com/taiki-e/cargo-llvm-cov#get-coverage-of-external-tests
|
||||
#
|
||||
# To display the coverage in your editor see:
|
||||
#
|
||||
# - https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters
|
||||
# - https://github.com/umaumax/vim-lcov
|
||||
# - https://github.com/andythigpen/nvim-coverage (probably needs some additional config)
|
||||
export def cov [] {
|
||||
let start = (date now)
|
||||
$env.NUSHELL_CARGO_PROFILE = "ci"
|
||||
|
||||
compute-coverage
|
||||
|
||||
let end = (date now)
|
||||
print $"Coverage generation took ($end - $start)."
|
||||
}
|
||||
|
||||
# Benchmark a target revision (default: current branch) against a reference revision (default: main branch)
|
||||
#
|
||||
# Results are saved in a `./tango` directory
|
||||
# Ensure you have `cargo-export` installed to generate separate artifacts for each branch.
|
||||
export def benchmark-compare [
|
||||
target?: string # which branch to compare (default: current branch)
|
||||
reference?: string # the reference to compare against (default: main branch)
|
||||
] {
|
||||
let reference = $reference | default "main"
|
||||
let current = git branch --show-current
|
||||
let target = $target | default $current
|
||||
|
||||
print $'-- Benchmarking ($target) against ($reference)'
|
||||
|
||||
let export_dir = $env.PWD | path join "tango"
|
||||
let ref_bin_dir = $export_dir | path join bin $reference
|
||||
let tgt_bin_dir = $export_dir | path join bin $target
|
||||
|
||||
# benchmark the target revision
|
||||
print $'-- Running benchmarks for ($target)'
|
||||
git checkout $target
|
||||
^cargo export $tgt_bin_dir -- bench
|
||||
|
||||
# benchmark the comparison reference revision
|
||||
print $'-- Running benchmarks for ($reference)'
|
||||
git checkout $reference
|
||||
^cargo export $ref_bin_dir -- bench
|
||||
|
||||
# return back to the whatever revision before benchmarking
|
||||
print '-- Done'
|
||||
git checkout $current
|
||||
|
||||
# report results
|
||||
let reference_bin = $ref_bin_dir | path join benchmarks
|
||||
let target_bin = $tgt_bin_dir | path join benchmarks
|
||||
^$target_bin compare $reference_bin -o -s 50 --dump ($export_dir | path join samples)
|
||||
}
|
||||
|
||||
# Benchmark the current branch and logs the result in `./tango/samples`
|
||||
#
|
||||
# Results are saved in a `./tango` directory
|
||||
# Ensure you have `cargo-export` installed to generate separate artifacts for each branch.
|
||||
export def benchmark-log [
|
||||
target?: string # which branch to compare (default: current branch)
|
||||
] {
|
||||
let current = git branch --show-current
|
||||
let target = $target | default $current
|
||||
print $'-- Benchmarking ($target)'
|
||||
|
||||
let export_dir = $env.PWD | path join "tango"
|
||||
let bin_dir = ($export_dir | path join bin $target)
|
||||
|
||||
# benchmark the target revision
|
||||
if $target != $current {
|
||||
git checkout $target
|
||||
}
|
||||
^cargo export $bin_dir -- bench
|
||||
|
||||
# return back to the whatever revision before benchmarking
|
||||
print '-- Done'
|
||||
if $target != $current {
|
||||
git checkout $current
|
||||
}
|
||||
|
||||
# report results
|
||||
let bench_bin = ($bin_dir | path join benchmarks)
|
||||
^$bench_bin compare -o -s 50 --dump ($export_dir | path join samples)
|
||||
}
|
||||
|
||||
# Build all Windows archives and MSIs for release manually
|
||||
#
|
||||
# This builds std and full distributions for both aarch64 and x86_64.
|
||||
#
|
||||
# You need to have the cross-compilers for MSVC installed (see Visual Studio).
|
||||
# If compiling on x86_64, you need ARM64 compilers and libs too, and vice versa.
|
||||
export def 'release-pkg windows' [
|
||||
--artifacts-dir="artifacts" # Where to copy the final msi and zip files to
|
||||
] {
|
||||
$env.RUSTFLAGS = ""
|
||||
$env.CARGO_TARGET_DIR = ""
|
||||
hide-env RUSTFLAGS
|
||||
hide-env CARGO_TARGET_DIR
|
||||
$env.OS = "windows-latest"
|
||||
$env.GITHUB_WORKSPACE = ("." | path expand)
|
||||
$env.GITHUB_OUTPUT = ("./output/out.txt" | path expand)
|
||||
let version = (open Cargo.toml | get package.version)
|
||||
mkdir $artifacts_dir
|
||||
for target in ["aarch64" "x86_64"] {
|
||||
$env.TARGET = $target ++ "-pc-windows-msvc"
|
||||
|
||||
rm -rf output
|
||||
_EXTRA_=bin nu .github/workflows/release-pkg.nu
|
||||
cp $"output/nu-($version)-($target)-pc-windows-msvc.zip" $artifacts_dir
|
||||
|
||||
rm -rf output
|
||||
_EXTRA_=msi nu .github/workflows/release-pkg.nu
|
||||
cp $"target/wix/nu-($version)-($target)-pc-windows-msvc.msi" $artifacts_dir
|
||||
}
|
||||
}
|
||||
|
||||
# these crates should compile for wasm
|
||||
const wasm_compatible_crates = [
|
||||
"nu-cmd-base",
|
||||
"nu-cmd-extra",
|
||||
"nu-cmd-lang",
|
||||
"nu-color-config",
|
||||
"nu-command",
|
||||
"nu-derive-value",
|
||||
"nu-engine",
|
||||
"nu-glob",
|
||||
"nu-json",
|
||||
"nu-parser",
|
||||
"nu-path",
|
||||
"nu-pretty-hex",
|
||||
"nu-protocol",
|
||||
"nu-std",
|
||||
"nu-system",
|
||||
"nu-table",
|
||||
"nu-term-grid",
|
||||
"nu-utils",
|
||||
"nuon"
|
||||
]
|
||||
|
||||
def "prep wasm" [] {
|
||||
^rustup target add wasm32-unknown-unknown
|
||||
}
|
||||
|
||||
# build crates for wasm
|
||||
export def "build wasm" [] {
|
||||
prep wasm
|
||||
|
||||
for crate in $wasm_compatible_crates {
|
||||
print $'(char nl)Building ($crate) for wasm'
|
||||
print '----------------------------'
|
||||
(
|
||||
^cargo build
|
||||
-p $crate
|
||||
--target wasm32-unknown-unknown
|
||||
--no-default-features
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
# make sure no api is used that doesn't work with wasm
|
||||
export def "clippy wasm" [] {
|
||||
prep wasm
|
||||
|
||||
$env.CLIPPY_CONF_DIR = $toolkit_dir | path join clippy wasm
|
||||
|
||||
for crate in $wasm_compatible_crates {
|
||||
print $'(char nl)Checking ($crate) for wasm'
|
||||
print '----------------------------'
|
||||
(
|
||||
^cargo clippy
|
||||
-p $crate
|
||||
--target wasm32-unknown-unknown
|
||||
--no-default-features
|
||||
--
|
||||
-D warnings
|
||||
-D clippy::unwrap_used
|
||||
-D clippy::unchecked_duration_subtraction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export def main [] { help toolkit }
|
||||
116
.gitignore
vendored
Normal file
116
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
.internal.git/
|
||||
.p
|
||||
.claude
|
||||
.vscode
|
||||
.shellcheckrc
|
||||
.coder
|
||||
.migration
|
||||
.zed
|
||||
# ai_demo.nu
|
||||
CLAUDE.md
|
||||
.cache
|
||||
.coder
|
||||
.wrks
|
||||
ROOT
|
||||
OLD
|
||||
old-config
|
||||
plugins/nushell-plugins
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
# Encryption keys and related files (CRITICAL - NEVER COMMIT)
|
||||
.k.backup
|
||||
*.key.backup
|
||||
|
||||
config.*.toml
|
||||
config.*back
|
||||
|
||||
# where book is written
|
||||
_book
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
node_modules/
|
||||
|
||||
**/output.css
|
||||
**/input.css
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
.env.development
|
||||
.env.staging
|
||||
|
||||
# Keep example files
|
||||
!.env.example
|
||||
|
||||
# Configuration files (may contain sensitive data)
|
||||
config.prod.toml
|
||||
config.production.toml
|
||||
config.local.toml
|
||||
config.*.local.toml
|
||||
|
||||
# Keep example configuration files
|
||||
!config.toml
|
||||
!config.dev.toml
|
||||
!config.example.toml
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# TLS certificates and keys
|
||||
certs/
|
||||
*.pem
|
||||
*.crt
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
*.tmp
|
||||
*~
|
||||
|
||||
# Encryption and security related files
|
||||
*.encrypted
|
||||
*.enc
|
||||
secrets/
|
||||
private/
|
||||
security/
|
||||
|
||||
# Configuration backups that may contain secrets
|
||||
config.*.backup
|
||||
config.backup.*
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
# Documentation build output
|
||||
book-output/
|
||||
# Generated setup report
|
||||
SETUP_COMPLETE.md
|
||||
|
||||
.coder/
|
||||
.claude/
|
||||
96
.markdownlint-cli2.jsonc
Normal file
96
.markdownlint-cli2.jsonc
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Markdownlint-cli2 Configuration
|
||||
// Documentation quality enforcement aligned with CLAUDE.md guidelines
|
||||
// See: https://github.com/igorshubovych/markdownlint-cli2
|
||||
|
||||
{
|
||||
"config": {
|
||||
"default": true,
|
||||
|
||||
// Headings - enforce proper hierarchy
|
||||
"MD001": false, // heading-increment (relaxed - allow flexibility)
|
||||
"MD026": { "punctuation": ".,;:!?" }, // heading-punctuation
|
||||
|
||||
// Lists - enforce consistency
|
||||
"MD004": { "style": "consistent" }, // ul-style (consistent list markers)
|
||||
"MD005": false, // inconsistent-indentation (relaxed)
|
||||
"MD007": { "indent": 2 }, // ul-indent
|
||||
"MD029": false, // ol-prefix (allow flexible list numbering)
|
||||
"MD030": { "ul_single": 1, "ol_single": 1, "ul_multi": 1, "ol_multi": 1 },
|
||||
|
||||
// Code blocks - fenced only
|
||||
"MD046": { "style": "fenced" }, // code-block-style
|
||||
|
||||
// Formatting - strict whitespace
|
||||
"MD009": true, // no-hard-tabs
|
||||
"MD010": true, // hard-tabs
|
||||
"MD011": true, // reversed-link-syntax
|
||||
"MD018": true, // no-missing-space-atx
|
||||
"MD019": true, // no-multiple-space-atx
|
||||
"MD020": true, // no-missing-space-closed-atx
|
||||
"MD021": true, // no-multiple-space-closed-atx
|
||||
"MD023": true, // heading-starts-line
|
||||
"MD027": true, // no-multiple-spaces-blockquote
|
||||
"MD037": true, // no-space-in-emphasis
|
||||
"MD039": true, // no-space-in-links
|
||||
|
||||
// Trailing content
|
||||
"MD012": false, // no-multiple-blanks (relaxed - allow formatting space)
|
||||
"MD024": false, // no-duplicate-heading (too strict for docs)
|
||||
"MD028": false, // no-blanks-blockquote (relaxed)
|
||||
"MD047": true, // single-trailing-newline
|
||||
|
||||
// Links and references
|
||||
"MD034": true, // no-bare-urls (links must be formatted)
|
||||
"MD040": true, // fenced-code-language (code blocks need language)
|
||||
"MD042": true, // no-empty-links
|
||||
|
||||
// HTML - allow for documentation formatting and images
|
||||
"MD033": { "allowed_elements": ["br", "hr", "details", "summary", "p", "img"] },
|
||||
|
||||
// Line length - relaxed for technical documentation
|
||||
"MD013": {
|
||||
"line_length": 150,
|
||||
"heading_line_length": 150,
|
||||
"code_block_line_length": 150,
|
||||
"code_blocks": true,
|
||||
"tables": true,
|
||||
"headers": true,
|
||||
"headers_line_length": 150,
|
||||
"strict": false,
|
||||
"stern": false
|
||||
},
|
||||
|
||||
// Images
|
||||
"MD045": true, // image-alt-text
|
||||
|
||||
// Disable rules that conflict with relaxed style
|
||||
"MD003": false, // consistent-indentation
|
||||
"MD041": false, // first-line-heading
|
||||
"MD025": false, // single-h1 / multiple-top-level-headings
|
||||
"MD022": false, // blanks-around-headings (flexible spacing)
|
||||
"MD032": false, // blanks-around-lists (flexible spacing)
|
||||
"MD035": false, // hr-style (consistent)
|
||||
"MD036": false, // no-emphasis-as-heading
|
||||
"MD044": false // proper-names
|
||||
},
|
||||
|
||||
// Documentation patterns
|
||||
"globs": [
|
||||
"docs/**/*.md",
|
||||
"!docs/node_modules/**",
|
||||
"!docs/build/**"
|
||||
],
|
||||
|
||||
// Ignore build artifacts, external content, and operational directories
|
||||
"ignores": [
|
||||
"node_modules/**",
|
||||
"target/**",
|
||||
".git/**",
|
||||
"build/**",
|
||||
"dist/**",
|
||||
".coder/**",
|
||||
".claude/**",
|
||||
".wrks/**",
|
||||
".vale/**"
|
||||
]
|
||||
}
|
||||
176
.pre-commit-config.yaml
Normal file
176
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Pre-commit Framework Configuration
|
||||
# Generated by dev-system/ci
|
||||
# Configures git pre-commit hooks for Rust projects
|
||||
|
||||
repos:
|
||||
# ============================================================================
|
||||
# Rust Hooks (COMMENTED OUT - Not used in this repo)
|
||||
# ============================================================================
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: rust-fmt
|
||||
# name: Rust formatting (cargo +nightly fmt)
|
||||
# entry: bash -c 'cargo +nightly fmt --all -- --check'
|
||||
# language: system
|
||||
# types: [rust]
|
||||
# pass_filenames: false
|
||||
# stages: [pre-commit]
|
||||
#
|
||||
# - id: rust-clippy
|
||||
# name: Rust linting (cargo clippy)
|
||||
# entry: bash -c 'cargo clippy --all-targets -- -D warnings'
|
||||
# language: system
|
||||
# types: [rust]
|
||||
# pass_filenames: false
|
||||
# stages: [pre-commit]
|
||||
#
|
||||
# - id: rust-test
|
||||
# name: Rust tests
|
||||
# entry: bash -c 'cargo test --workspace'
|
||||
# language: system
|
||||
# types: [rust]
|
||||
# pass_filenames: false
|
||||
# stages: [pre-push]
|
||||
#
|
||||
# - id: cargo-deny
|
||||
# name: Cargo deny (licenses & advisories)
|
||||
# entry: bash -c 'cargo deny check licenses advisories'
|
||||
# language: system
|
||||
# pass_filenames: false
|
||||
# stages: [pre-push]
|
||||
|
||||
# ============================================================================
|
||||
# Nushell Hooks (ACTIVE)
|
||||
# ============================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: nushell-check
|
||||
name: Nushell validation (nu --ide-check)
|
||||
entry: >-
|
||||
bash -c 'for f in $(git diff --cached --name-only --diff-filter=ACM | grep "\.nu$"); do
|
||||
echo "Checking: $f"; nu --ide-check 100 "$f" || exit 1; done'
|
||||
language: system
|
||||
types: [file]
|
||||
files: \.nu$
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
# ============================================================================
|
||||
# Nickel Hooks (ACTIVE)
|
||||
# ============================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: nickel-typecheck
|
||||
name: Nickel type checking
|
||||
entry: >-
|
||||
bash -c 'export NICKEL_IMPORT_PATH="../:."; for f in $(git diff --cached --name-only --diff-filter=ACM | grep "\.ncl$"); do
|
||||
echo "Checking: $f"; nickel typecheck "$f" || exit 1; done'
|
||||
language: system
|
||||
types: [file]
|
||||
files: \.ncl$
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
# ============================================================================
|
||||
# Bash Hooks (optional - enable if using Bash)
|
||||
# ============================================================================
|
||||
# - repo: local
|
||||
# hooks:
|
||||
# - id: shellcheck
|
||||
# name: Shellcheck (bash linting)
|
||||
# entry: shellcheck
|
||||
# language: system
|
||||
# types: [shell]
|
||||
# stages: [commit]
|
||||
#
|
||||
# - id: shfmt
|
||||
# name: Shell script formatting
|
||||
# entry: bash -c 'shfmt -i 2 -d'
|
||||
# language: system
|
||||
# types: [shell]
|
||||
# stages: [commit]
|
||||
|
||||
# ============================================================================
|
||||
# Markdown Hooks (ACTIVE)
|
||||
# ============================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
name: Markdown linting (markdownlint-cli2)
|
||||
entry: markdownlint-cli2
|
||||
language: system
|
||||
types: [markdown]
|
||||
stages: [pre-commit]
|
||||
|
||||
# CRITICAL: markdownlint-cli2 MD040 only checks opening fences for language.
|
||||
# It does NOT catch malformed closing fences (e.g., ```plaintext) - CommonMark violation.
|
||||
# This hook is ESSENTIAL to prevent malformed closing fences from entering the repo.
|
||||
# See: .markdownlint-cli2.jsonc line 22-24 for details.
|
||||
- id: check-malformed-fences
|
||||
name: Check malformed closing fences (CommonMark)
|
||||
entry: bash -c 'cd .. && nu scripts/check-malformed-fences.nu $(git diff --cached --name-only --diff-filter=ACM | grep "\.md$" | grep -v ".coder/" | grep -v ".claude/" | grep -v "old_config/" | tr "\n" " ")'
|
||||
language: system
|
||||
types: [markdown]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
exclude: ^\.coder/|^\.claude/|^old_config/
|
||||
|
||||
# ============================================================================
|
||||
# General Pre-commit Hooks
|
||||
# ============================================================================
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
|
||||
- id: check-case-conflict
|
||||
|
||||
- id: check-merge-conflict
|
||||
|
||||
- id: check-toml
|
||||
|
||||
# - id: check-yaml
|
||||
# exclude: ^\.woodpecker/
|
||||
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^(\.coder/|\.wrks/|\.claude/)
|
||||
|
||||
- id: trailing-whitespace
|
||||
exclude: \.md$|^(\.coder/|\.wrks/|\.claude/)
|
||||
|
||||
- id: mixed-line-ending
|
||||
exclude: ^(\.coder/|\.wrks/|\.claude/)
|
||||
|
||||
# ============================================================================
|
||||
# ADR-026 nulib structural constraints (M6)
|
||||
# ============================================================================
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: nulib-no-relative-imports-new-tree
|
||||
name: "ADR-026: no relative imports in new nulib layers"
|
||||
entry: bash -c 'found=$(git diff --cached --name-only | grep "nulib/(cli|orchestration|platform|domain|tools|primitives|extensions)/" | xargs grep -l "^use \.\." 2>/dev/null); [ -z "$found" ] && exit 0; echo "Relative imports found in new tree:"; echo "$found"; exit 1'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: nulib-single-nickel-export
|
||||
name: "ADR-026: ^nickel export only in tools/nickel/process.nu"
|
||||
entry: bash -c 'found=$(git diff --cached --name-only | grep "nulib/.*\.nu$" | grep -v "nulib/tools/nickel/process.nu" | xargs grep -l "\\^nickel export" 2>/dev/null); [ -z "$found" ] && exit 0; echo "^nickel export outside tools/nickel/process.nu:"; echo "$found"; exit 1'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: nulib-no-surrealdb
|
||||
name: "ADR-026: no surrealdb calls in nulib"
|
||||
entry: bash -c 'found=$(git diff --cached --name-only | grep "nulib/.*\.nu$" | xargs grep -l "surrealdb\|surreal\b" 2>/dev/null); [ -z "$found" ] && exit 0; echo "surrealdb in nulib:"; echo "$found"; exit 1'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
|
||||
- id: nulib-no-star-imports-new-tree
|
||||
name: "ADR-026: no star imports (use X *) in new nulib layers"
|
||||
entry: bash -c 'found=$(git diff --cached --name-only | grep "nulib/(cli|orchestration|platform|domain|tools|primitives|extensions)/.*\.nu$" | xargs grep -lP "^use \S+ \*$" 2>/dev/null | grep -v "mod\.nu"); [ -z "$found" ] && exit 0; echo "Star imports in new tree (outside mod.nu):"; echo "$found"; exit 1'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
381
CHANGELOG.md
Normal file
381
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
# Provisioning Core - Changelog
|
||||
|
||||
**Date**: 2026-04-17
|
||||
**Repository**: provisioning/core
|
||||
**Status**: Nickel IaC (PRIMARY)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
Major refactor: three-layer DAG architecture with workspace composition, Unified
|
||||
Component Architecture (components + workflows + capabilities), Nickel-backed
|
||||
commands-registry with JSON cache for fast CLI startup, consolidated platform
|
||||
service manager, and completed Nushell 0.110/0.112 migration (no try/catch, no
|
||||
bash redirections). TTY stack moved from `shlib/` into `cli/tty-*`. Numerous new
|
||||
domain modules: `dag`, `components`, `workflow` engine, `images` lifecycle,
|
||||
workspace state/sync, ontoref queries, FIP handler.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Latest Release (2026-04-17)
|
||||
|
||||
### Three-Layer DAG Architecture
|
||||
|
||||
**Scope**: Workspace composition as a DAG with formula_id::task_name namespacing,
|
||||
health gates, conditions, and NATS subject emission.
|
||||
|
||||
**New files**:
|
||||
- `nulib/main_provisioning/dag.nu` — `dag show/validate/export` (DOT/JSON/Mermaid)
|
||||
- `nulib/lib_provisioning/config/loader/dag.nu` — DAG config loader
|
||||
- `nulib/taskservs/dag-executor.nu` — taskserv-level DAG execution helper
|
||||
|
||||
**Related**: ADR-020 (extension capability declarations), ADR-021 (workspace
|
||||
composition DAG). Orchestrator consumes composition via
|
||||
`WorkspaceComposition::into_workflow` and emits NATS events.
|
||||
|
||||
### Unified Component Architecture
|
||||
|
||||
**Scope**: Components + workflows + capabilities as first-class citizens
|
||||
(libre-daoshi plan, blocks A-H complete).
|
||||
|
||||
**New files**:
|
||||
- `nulib/components/mod.nu` — component dispatch module
|
||||
- `nulib/main_provisioning/components.nu` — `validate capabilities/components`,
|
||||
`component list/info`
|
||||
- `nulib/main_provisioning/workflow.nu` — full workflow engine: run/list/status/
|
||||
validate, topological sort, NATS event emission (+605 lines)
|
||||
- `nulib/main_provisioning/extensions.nu` — `extensions capabilities/graph`
|
||||
- `nulib/main_provisioning/ontoref-queries.nu` — on+re-aware CLI queries
|
||||
(describe component/databases/namespace/storage/workflow)
|
||||
|
||||
### Commands-Registry & Fast-Path Dispatch
|
||||
|
||||
**Scope**: Eliminate Nu startup cost on every `prvng` invocation.
|
||||
|
||||
**New files**:
|
||||
- `nulib/commands-registry.ncl` — Nickel command catalog (314 lines)
|
||||
- `nulib/lib_provisioning/utils/command-registry.nu` — registry accessor
|
||||
- `nulib/scripts/validate-command.nu` — cache-aware command validator
|
||||
|
||||
**Behavior**: `cli/provisioning` reads the JSON cache at
|
||||
`~/.cache/provisioning/commands-registry.json`, rebuilt automatically via
|
||||
`nickel export` when the `.ncl` source is newer. Single-char aliases
|
||||
(`s`, `t`, `c`, `e`, `w`, `j`, `b`, `o`, `a`) are expanded in bash before
|
||||
dispatch. `nulib/main_provisioning/ADDING_COMMANDS.md` documents the four-step
|
||||
procedure for new commands.
|
||||
|
||||
### Platform Service Manager
|
||||
|
||||
**New files**:
|
||||
- `nulib/lib_provisioning/platform/service-manager.nu` (+573 lines)
|
||||
- `nulib/lib_provisioning/platform/startup.nu` (+611 lines)
|
||||
- `nulib/lib_provisioning/utils/service-check.nu` (+255 lines)
|
||||
|
||||
**Refactored**: `platform/autostart.nu`, `platform/bootstrap.nu`,
|
||||
`platform/health.nu`, `platform/target.nu` — unified lifecycle, health probes,
|
||||
and autostart logic.
|
||||
|
||||
### Nushell 0.112.2 Migration
|
||||
|
||||
**Scope**: Project-wide refactor driven by `scripts/refactor-try-catch.nu` and
|
||||
`scripts/refactor-try-catch-simplified.nu` to reach Nushell 0.112.2 compliance.
|
||||
|
||||
**Enforced**:
|
||||
- No `try/catch` — all use `do { } | complete`
|
||||
- No bash redirections (`2>&1`, `2>/dev/null`)
|
||||
- External commands prefixed with `^`
|
||||
- Parenthesized pipelines in `if`
|
||||
- Type signatures: `def f [x: string]: nothing -> record { }`
|
||||
|
||||
### TTY Stack Replacement
|
||||
|
||||
**Removed**: `shlib/README.md`, `shlib/auth-login-tty.sh`,
|
||||
`shlib/mfa-enroll-tty.sh`, `shlib/setup-wizard-tty.sh`.
|
||||
|
||||
**Replaced by**:
|
||||
- `cli/tty-dispatch.sh` (+86 lines) — TTY-safe command dispatcher
|
||||
- `cli/tty-filter.sh` (+137 lines) — command filter
|
||||
- `cli/tty-commands.conf` — TTY command manifest
|
||||
|
||||
### New Domain Modules
|
||||
|
||||
- `nulib/images/` — golden image lifecycle (`create`, `delete`, `list`, `state`,
|
||||
`update`, `watch`)
|
||||
- `nulib/workspace/state.nu` (+641 lines) — workspace state model
|
||||
- `nulib/workspace/sync.nu` (+148 lines) — workspace synchronization
|
||||
- `nulib/main_provisioning/bootstrap.nu` — platform bootstrap
|
||||
- `nulib/main_provisioning/cluster-deploy.nu` — component/taskserv dispatch
|
||||
- `nulib/main_provisioning/fip.nu` (+421 lines) — floating IP handler
|
||||
- `nulib/main_provisioning/state.nu` — state command
|
||||
- `nulib/main_provisioning/commands/state.nu`, `commands/build.nu`,
|
||||
`commands/integrations/auth.nu`, `commands/utilities/alias.nu`
|
||||
- `nulib/main_provisioning/commands/platform.nu` — major expansion (+874 lines)
|
||||
|
||||
### Config Loader Overhaul
|
||||
|
||||
- `lib_provisioning/config/loader/core.nu` — slimmed (−759 lines of legacy paths)
|
||||
- `lib_provisioning/config/cache/core.nu` — refactored (−454 lines of dead paths)
|
||||
- `lib_provisioning/config/cache/nickel.nu` — simplified
|
||||
- Removed: `lib_provisioning/config/loaders/file_loader.nu` (−330 lines)
|
||||
- Added: `config/accessor-minimal.nu`, `config/accessor/functions.nu` helpers
|
||||
|
||||
### Scripts & Tooling
|
||||
|
||||
- `nulib/scripts/` — query-* family (clusters/infra/providers/servers/taskservs/
|
||||
workspace-info), validate-command, validate-config
|
||||
- `scripts/auto-refactor-priority.nu`, `scripts/batch-refactor.sh`
|
||||
- `scripts/build-nixos-image-remote.sh`, `scripts/deploy-cp-server.sh`
|
||||
|
||||
### CLI Modular Subcommands
|
||||
|
||||
New top-level Nu modules referenced by the bash dispatcher:
|
||||
`provisioning-batch.nu`, `provisioning-bootstrap.nu`, `provisioning-cluster.nu`,
|
||||
`provisioning-component.nu`, `provisioning-extension.nu`, `provisioning-job.nu`,
|
||||
`provisioning-platform.nu`, `provisioning-server.nu`, `provisioning-state.nu`,
|
||||
`provisioning-status.nu`, `provisioning-taskserv.nu`, `provisioning-volume.nu`,
|
||||
`provisioning-workflow.nu`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `nulib/tests/test_workspace_state.nu` (+351 lines)
|
||||
- Updates to `test_oci_registry.nu`, `test_services.nu`
|
||||
|
||||
### Statistics
|
||||
|
||||
| Area | Files | Lines +/− |
|
||||
| ---- | ----- | --------- |
|
||||
| DAG + Components + Workflows | 8 | +1800 / −50 |
|
||||
| Commands-registry + dispatch | 6 | +900 / −200 |
|
||||
| Platform service manager | 5 | +1700 / −300 |
|
||||
| Config loader/cache refactor | 10 | +400 / −1500 |
|
||||
| TTY replacement | 4 | +250 / −515 |
|
||||
| New subcommand modules | 13 | +1700 / −0 |
|
||||
| **Total staged** | **242 files** | **+21949 / −6012** |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Previous Release (2026-01-14)
|
||||
|
||||
### Terminology Migration: Cluster → Taskserv
|
||||
|
||||
**Scope**: Complete refactoring across nulib/ modules to standardize on taskserv nomenclature
|
||||
|
||||
**Files Updated**:
|
||||
- `nulib/clusters/handlers.nu` - Handler signature updates, ANSI formatting improvements
|
||||
- `nulib/clusters/run.nu` - Function parameter and path updates (+326 lines modified)
|
||||
- `nulib/clusters/utils.nu` - Utility function updates (+144 lines modified)
|
||||
- `nulib/clusters/discover.nu` - Discovery module refactoring
|
||||
- `nulib/clusters/load.nu` - Configuration loader updates
|
||||
- `nulib/ai/query_processor.nu` - AI integration updates
|
||||
- `nulib/api/routes.nu` - API routing adjustments
|
||||
- `nulib/api/server.nu` - Server module updates
|
||||
- `.pre-commit-config.yaml` - Pre-commit hook updates
|
||||
|
||||
**Changes**:
|
||||
- Updated function parameters: `server_cluster_path` → `server_taskserv_path`
|
||||
- Updated record fields: `defs.cluster.name` → `defs.taskserv.name`
|
||||
- Enhanced output formatting with consistent ANSI styling (yellow_bold, default_dimmed, purple_bold)
|
||||
- Improved function documentation and import organization
|
||||
- Pre-commit configuration refinements
|
||||
|
||||
**Rationale**: Taskserv better reflects the service-oriented nature of infrastructure components and improves semantic clarity throughout the codebase.
|
||||
|
||||
### i18n/Localization System
|
||||
|
||||
**New Feature**: Fluent i18n integration for internationalized help system
|
||||
|
||||
**Implementation**:
|
||||
- `nulib/main_provisioning/help_system_fluent.nu` - Fluent-based i18n framework
|
||||
- Active locale detection from `LANG` environment variable
|
||||
- Fallback to English (en-US) for missing translations
|
||||
- Fluent catalog parsing: `locale/{locale}/help.ftl`
|
||||
- Locale format conversion: `es_ES.UTF-8` → `es-ES`
|
||||
|
||||
**Features**:
|
||||
- Automatic locale detection from system LANG
|
||||
- Fluent catalog format support for translations
|
||||
- Graceful fallback mechanism
|
||||
- Category-based color formatting (infrastructure, orchestration, development, etc.)
|
||||
- Tab-separated help column formatting
|
||||
|
||||
---
|
||||
|
||||
## 📋 Version History
|
||||
|
||||
### v1.0.10 (Previous Release)
|
||||
- Stable release with Nickel IaC support
|
||||
- Base version with core CLI and library system
|
||||
|
||||
### v1.0.11 (Current - 2026-01-14)
|
||||
- **Cluster → Taskserv** terminology migration
|
||||
- **Fluent i18n** system documentation
|
||||
- Enhanced ANSI output formatting
|
||||
|
||||
---
|
||||
|
||||
## 📁 Changes by Directory
|
||||
|
||||
### cli/ directory
|
||||
|
||||
**Major Updates (586 lines added to provisioning)**
|
||||
|
||||
- Expanded CLI command implementations (+590 lines)
|
||||
- Enhanced tools installation system (tools-install: +163 lines)
|
||||
- Improved install script for Nushell environment (install_nu.sh: +31 lines)
|
||||
- Better CLI routing and command validation
|
||||
- Help system enhancements for Nickel-aware commands
|
||||
- Support for Nickel schema evaluation and validation
|
||||
|
||||
### nulib/ directory
|
||||
|
||||
**Nushell libraries - Nickel-first architecture**
|
||||
|
||||
**Config System**
|
||||
- `config/loader.nu` - Nickel schema loading and evaluation
|
||||
- `config/accessor.nu` - Accessor patterns for Nickel fields
|
||||
- `config/cache/` - Cache system optimized for Nickel evaluation
|
||||
|
||||
**AI & Documentation**
|
||||
- `ai/README.md` - Nickel IaC patterns
|
||||
- `ai/info_about.md` - Nickel-focused documentation
|
||||
- `ai/lib.nu` - AI integration for Nickel schema analysis
|
||||
|
||||
**Extension System**
|
||||
- `extensions/QUICKSTART.md` - Nickel extension quickstart (+50 lines)
|
||||
- `extensions/README.md` - Extension system for Nickel (+63 lines)
|
||||
- `extensions/loader_oci.nu` - OCI registry loader (minor updates)
|
||||
|
||||
**Infrastructure & Validation**
|
||||
- `infra_validator/rules_engine.nu` - Validation rules for Nickel schemas
|
||||
- `infra_validator/validator.nu` - Schema validation support
|
||||
- `loader-minimal.nu` - Minimal loader for lightweight deployments
|
||||
|
||||
**Clusters & Workflows**
|
||||
- `clusters/discover.nu`, `clusters/load.nu`, `clusters/run.nu` - Cluster operations updated
|
||||
- Plugin definitions updated for Nickel integration (+28-38 lines)
|
||||
|
||||
**Documentation**
|
||||
- `SERVICE_MANAGEMENT_SUMMARY.md` - Expanded service documentation (+90 lines)
|
||||
- `gitea/IMPLEMENTATION_SUMMARY.md` - Gitea integration guide (+89 lines)
|
||||
- Extension and validation quickstarts and README updates
|
||||
|
||||
### plugins/ directory
|
||||
|
||||
Nushell plugins for performance optimization
|
||||
|
||||
**Sub-repositories:**
|
||||
|
||||
- `nushell-plugins/` - Multiple Nushell plugins
|
||||
- `_nu_plugin_inquire/` - Interactive form plugin
|
||||
- `api_nu_plugin_nickel/` - Nickel integration plugin
|
||||
- Additional plugin implementations
|
||||
|
||||
**Plugin Documentation:**
|
||||
|
||||
- Build summaries
|
||||
- Installation guides
|
||||
- Configuration examples
|
||||
- Test documentation
|
||||
- Fix and limitation reports
|
||||
|
||||
### scripts/ directory
|
||||
|
||||
Utility scripts for system operations
|
||||
|
||||
- Build scripts
|
||||
- Installation scripts
|
||||
- Testing scripts
|
||||
- Development utilities
|
||||
- Infrastructure scripts
|
||||
|
||||
### services/ directory
|
||||
|
||||
Service definitions and configurations
|
||||
|
||||
- Service descriptions
|
||||
- Service management
|
||||
|
||||
### forminquire/ directory (ARCHIVED)
|
||||
|
||||
**Status**: DEPRECATED - Archived to `.coder/archive/forminquire/`
|
||||
|
||||
**Replacement**: TypeDialog forms (`.typedialog/provisioning/`)
|
||||
|
||||
- Legacy: Jinja2-based form system
|
||||
- Archived: 2025-01-09
|
||||
- Replaced by: TypeDialog with bash wrappers for TTY-safe input
|
||||
|
||||
### Additional Files
|
||||
|
||||
- `README.md` - Core system documentation
|
||||
- `versions.ncl` - Version definitions
|
||||
- `.gitignore` - Git ignore patterns
|
||||
- `nickel.mod` / `nickel.mod.lock` - Nickel module definitions
|
||||
- `.githooks/` - Git hooks for development
|
||||
|
||||
---
|
||||
|
||||
## 📊 Change Statistics
|
||||
|
||||
| Category | Files | Lines Added | Lines Removed | Status |
|
||||
| -------- | ----- | ----------- | ------------- | ------ |
|
||||
| CLI | 3 | 780+ | 30+ | Major update |
|
||||
| Config System | 15+ | 300+ | 200+ | Refactored |
|
||||
| AI/Docs | 8+ | 350+ | 100+ | Enhanced |
|
||||
| Extensions | 5+ | 150+ | 50+ | Updated |
|
||||
| Infrastructure | 8+ | 100+ | 70+ | Updated |
|
||||
| Clusters/Workflows | 5+ | 80+ | 30+ | Enhanced |
|
||||
| **Total** | **60+ files** | **1700+ lines** | **500+ lines** | **Complete** |
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Areas
|
||||
|
||||
### CLI System
|
||||
|
||||
- Command implementations with Nickel support
|
||||
- Tools installation system
|
||||
- Nushell environment setup
|
||||
- Nickel schema evaluation commands
|
||||
- Error messages and help text
|
||||
- Nickel type checking and validation
|
||||
|
||||
### Config System
|
||||
|
||||
- **Nickel-first loader**: Schema evaluation via config/loader.nu
|
||||
- **Optimized caching**: Nickel evaluation cache patterns
|
||||
- **Field accessors**: Nickel record manipulation
|
||||
- **Schema validation**: Type-safe configuration loading
|
||||
|
||||
### AI & Documentation
|
||||
|
||||
- AI integration for Nickel IaC
|
||||
- Extension development guides
|
||||
- Service management documentation
|
||||
|
||||
### Extensions & Infrastructure
|
||||
|
||||
- OCI registry loader optimization
|
||||
- Schema-aware extension system
|
||||
- Infrastructure validation for Nickel definitions
|
||||
- Cluster discovery and operations enhanced
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Current Features
|
||||
|
||||
- **Nickel IaC**: Type-safe infrastructure definitions
|
||||
- **CLI System**: Unified command interface with 80+ shortcuts
|
||||
- **Provider Abstraction**: Cloud-agnostic operations
|
||||
- **Config System**: Hierarchical configuration with 476+ accessors
|
||||
- **Workflow Engine**: Batch operations with dependency resolution
|
||||
- **Validation**: Schema-aware infrastructure validation
|
||||
- **AI Integration**: Schema-driven configuration generation
|
||||
|
||||
---
|
||||
|
||||
**Status**: Production
|
||||
**Date**: 2026-01-14
|
||||
**Repository**: provisioning/core
|
||||
**Version**: 1.0.11
|
||||
109
CODE_OF_CONDUCT.md
Normal file
109
CODE_OF_CONDUCT.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We, as members, contributors, and leaders, pledge to make participation in our project and community
|
||||
a harassment-free experience for everyone, regardless of:
|
||||
|
||||
- Age
|
||||
- Body size
|
||||
- Visible or invisible disability
|
||||
- Ethnicity
|
||||
- Sex characteristics
|
||||
- Gender identity and expression
|
||||
- Level of experience
|
||||
- Education
|
||||
- Socioeconomic status
|
||||
- Nationality
|
||||
- Personal appearance
|
||||
- Race
|
||||
- Caste
|
||||
- Color
|
||||
- Religion
|
||||
- Sexual identity and orientation
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by mistakes
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery
|
||||
- Trolling, insulting, or derogatory comments
|
||||
- Personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information (doxing)
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior
|
||||
and will take appropriate corrective action in response to unacceptable behavior.
|
||||
|
||||
Maintainers have the right and responsibility to:
|
||||
|
||||
- Remove, edit, or reject comments, commits, code, and other contributions
|
||||
- Ban contributors for behavior they deem inappropriate, threatening, or harmful
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies to:
|
||||
|
||||
- All community spaces (GitHub, forums, chat, events, etc.)
|
||||
- Official project channels and representations
|
||||
- Interactions between community members related to the project
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to project maintainers:
|
||||
|
||||
- Email: [project contact]
|
||||
- GitHub: Private security advisory
|
||||
- Issues: Report with `conduct` label (public discussions only)
|
||||
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
### Enforcement Guidelines
|
||||
|
||||
**1. Correction**
|
||||
|
||||
- Community impact: Use of inappropriate language or unwelcoming behavior
|
||||
- Action: Private written warning with explanation and clarity on impact
|
||||
- Consequence: Warning and no further violations
|
||||
|
||||
**2. Warning**
|
||||
|
||||
- Community impact: Violation through single incident or series of actions
|
||||
- Action: Written warning with severity consequences for continued behavior
|
||||
- Consequence: Suspension from community interaction
|
||||
|
||||
**3. Temporary Ban**
|
||||
|
||||
- Community impact: Serious violation of standards
|
||||
- Action: Temporary ban from community interaction
|
||||
- Consequence: Revocation of ban after reflection period
|
||||
|
||||
**4. Permanent Ban**
|
||||
|
||||
- Community impact: Pattern of violating community standards
|
||||
- Action: Permanent ban from community interaction
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at <https://www.contributor-covenant.org/faq>.
|
||||
|
||||
---
|
||||
|
||||
**Thank you for being part of our community!**
|
||||
|
||||
We believe in creating a welcoming and inclusive space where everyone can contribute their best work. Together, we make this project better.
|
||||
131
CONTRIBUTING.md
Normal file
131
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Contributing to provisioning
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to this project.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project adheres to a Code of Conduct. By participating, you are expected to uphold this code.
|
||||
Please see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Rust 1.70+ (if project uses Rust)
|
||||
- NuShell (if project uses Nushell scripts)
|
||||
- Git
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork: `git clone https://repo.jesusperez.pro/jesus/provisioning`
|
||||
3. Add upstream: `git remote add upstream https://repo.jesusperez.pro/jesus/provisioning`
|
||||
4. Create a branch: `git checkout -b feature/your-feature`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Before You Code
|
||||
|
||||
- Check existing issues and pull requests to avoid duplication
|
||||
- Create an issue to discuss major changes before implementing
|
||||
- Assign yourself to let others know you're working on it
|
||||
|
||||
### Code Standards
|
||||
|
||||
#### Rust
|
||||
|
||||
- Run `cargo fmt --all` before committing
|
||||
- All code must pass `cargo clippy -- -D warnings`
|
||||
- Write tests for new functionality
|
||||
- Maintain 100% documentation coverage for public APIs
|
||||
|
||||
#### Nushell
|
||||
|
||||
- Validate scripts with `nu --ide-check 100 script.nu`
|
||||
- Follow consistent naming conventions
|
||||
- Use type hints where applicable
|
||||
|
||||
#### Nickel
|
||||
|
||||
- Type check schemas with `nickel typecheck`
|
||||
- Document schema fields with comments
|
||||
- Test schema validation
|
||||
|
||||
### Commit Guidelines
|
||||
|
||||
- Write clear, descriptive commit messages
|
||||
- Reference issues with `Fixes #123` or `Related to #123`
|
||||
- Keep commits focused on a single concern
|
||||
- Use imperative mood: "Add feature" not "Added feature"
|
||||
|
||||
### Testing
|
||||
|
||||
All changes must include tests:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test --workspace
|
||||
|
||||
# Run with coverage
|
||||
cargo llvm-cov --all-features --lcov
|
||||
|
||||
# Run locally before pushing
|
||||
just ci-full
|
||||
```
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. Update documentation for any changed functionality
|
||||
2. Add tests for new code
|
||||
3. Ensure all CI checks pass
|
||||
4. Request review from maintainers
|
||||
5. Be responsive to feedback and iterate quickly
|
||||
|
||||
## Review Process
|
||||
|
||||
- Maintainers will review your PR within 3-5 business days
|
||||
- Feedback is constructive and meant to improve the code
|
||||
- All discussions should be respectful and professional
|
||||
- Once approved, maintainers will merge the PR
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
Found a bug? Please file an issue with:
|
||||
|
||||
- **Title**: Clear, descriptive title
|
||||
- **Description**: What happened and what you expected
|
||||
- **Steps to reproduce**: Minimal reproducible example
|
||||
- **Environment**: OS, Rust version, etc.
|
||||
- **Screenshots**: If applicable
|
||||
|
||||
## Suggesting Enhancements
|
||||
|
||||
Have an idea? Please file an issue with:
|
||||
|
||||
- **Title**: Clear feature title
|
||||
- **Description**: What, why, and how
|
||||
- **Use cases**: Real-world scenarios where this would help
|
||||
- **Alternative approaches**: If you've considered any
|
||||
|
||||
## Documentation
|
||||
|
||||
- Keep README.md up to date
|
||||
- Document public APIs with rustdoc comments
|
||||
- Add examples for non-obvious functionality
|
||||
- Update CHANGELOG.md with your changes
|
||||
|
||||
## Release Process
|
||||
|
||||
Maintainers handle releases following semantic versioning:
|
||||
|
||||
- MAJOR: Breaking changes
|
||||
- MINOR: New features (backward compatible)
|
||||
- PATCH: Bug fixes
|
||||
|
||||
## Questions
|
||||
|
||||
- Check existing documentation and issues
|
||||
- Ask in discussions or open an issue
|
||||
- Join our community channels
|
||||
|
||||
Thank you for contributing!
|
||||
0
cli/.gitkeep
Normal file
0
cli/.gitkeep
Normal file
467
cli/README.md
Normal file
467
cli/README.md
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
# Provisioning CLI - Flow-Aware TTY Command Management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The provisioning wrapper (`provisioning/core/cli/provisioning`) is a **flow controller** that manages three execution paths for command handling:
|
||||
|
||||
1. **Standalone TTY** - Interactive commands that exit after execution
|
||||
2. **Pipeline TTY** - Interactive commands that output for piping to other commands
|
||||
3. **Regular** - Standard Nushell command processing
|
||||
|
||||
This design enables:
|
||||
- Interactive commands (TTY input) without blocking Nushell
|
||||
- Inter-command piping of TTY output to subsequent commands
|
||||
- Same-command flow (TTY input → Nushell processing in one execution)
|
||||
- Daemon optimization for non-interactive commands
|
||||
|
||||
## How Flow Management Works
|
||||
|
||||
### Execution Flow
|
||||
|
||||
```text
|
||||
User Command: provisioning <cmd> <args>
|
||||
↓
|
||||
Bash wrapper (provisioning)
|
||||
↓
|
||||
┌──────────────────────────────────────┐
|
||||
│ Phase 1: TTY Command Detection │
|
||||
│ - Read tty-commands.conf registry │
|
||||
│ - Match command pattern │
|
||||
└──────────────────────────────────────┘
|
||||
↓
|
||||
├─→ Not a TTY command → Continue to Nushell (normal processing)
|
||||
│
|
||||
└─→ TTY command found → Check flow type
|
||||
↓
|
||||
├─→ flow=exit → Execute wrapper, exit immediately
|
||||
├─→ flow=pipe → Execute wrapper, output to stdout, exit (allows piping)
|
||||
└─→ flow=continue → Execute wrapper, capture output, continue to Nushell
|
||||
($env.TTY_OUTPUT available in Nushell)
|
||||
```
|
||||
|
||||
### Flow Types Explained
|
||||
|
||||
#### 1. Standalone TTY Commands (flow=exit)
|
||||
|
||||
**Use case**: Interactive forms, setup wizards, authentication dialogs
|
||||
|
||||
**Example**: `provisioning setup wizard`
|
||||
|
||||
**Flow**:
|
||||
|
||||
```bash
|
||||
Bash wrapper → TTY filter detects "setup wizard" → flow=exit
|
||||
↓
|
||||
Execute wrapper: core/shlib/setup-wizard-tty.sh
|
||||
↓
|
||||
User interaction (TypeDialog form)
|
||||
↓
|
||||
Exit wrapper → Exit bash wrapper
|
||||
↓
|
||||
Never reaches Nushell
|
||||
```
|
||||
|
||||
**Registry entry**:
|
||||
|
||||
```bash
|
||||
"setup wizard" "core/shlib/setup-wizard-tty.sh" "exit"
|
||||
```
|
||||
|
||||
#### 2. Pipeline TTY Commands (flow=pipe)
|
||||
|
||||
**Use case**: Getting user input to pipe to another command
|
||||
|
||||
**Example**: `provisioning auth get-key | provisioning deploy --api-key-stdin`
|
||||
|
||||
**Flow**:
|
||||
|
||||
```bash
|
||||
Bash wrapper → TTY filter detects "auth get-key" → flow=pipe
|
||||
↓
|
||||
Execute wrapper: core/shlib/auth-get-key-tty.sh
|
||||
↓
|
||||
User provides API key via TTY prompt
|
||||
↓
|
||||
Wrapper outputs API key to stdout
|
||||
↓
|
||||
Exit wrapper (process exits, pipe has captured output)
|
||||
↓
|
||||
Next command receives API key from stdin
|
||||
```
|
||||
|
||||
**Registry entry**:
|
||||
|
||||
```bash
|
||||
"auth get-key" "core/shlib/auth-get-key-tty.sh" "pipe"
|
||||
```
|
||||
|
||||
**Wrapper requirements** (flow=pipe):
|
||||
- Must output result to stdout
|
||||
- Output must be newline-terminated
|
||||
- Exit with proper code (0=success, non-zero=error)
|
||||
|
||||
#### 3. Continue-to-Nushell TTY Commands (flow=continue)
|
||||
|
||||
**Use case**: TTY input that needs further processing in Nushell
|
||||
|
||||
**Example**: `provisioning auth integrate --provider azure`
|
||||
|
||||
**Flow**:
|
||||
|
||||
```bash
|
||||
Bash wrapper → TTY filter detects "auth integrate" → flow=continue
|
||||
↓
|
||||
Execute wrapper: core/shlib/auth-integrate-tty.sh
|
||||
↓
|
||||
User provides credentials via TTY prompt
|
||||
↓
|
||||
Wrapper outputs credentials (usually JSON) to stdout
|
||||
↓
|
||||
Filter CAPTURES output to $TTY_OUTPUT environment variable
|
||||
↓
|
||||
Set $env.PROVISIONING_BYPASS_DAEMON=true (skip daemon)
|
||||
↓
|
||||
Return 0 WITHOUT EXITING (continue to Nushell)
|
||||
↓
|
||||
Nushell dispatcher receives both:
|
||||
- CLI args: --provider azure
|
||||
- TTY output: $env.TTY_OUTPUT (credentials JSON)
|
||||
↓
|
||||
Nushell script processes both, completes integration
|
||||
```
|
||||
|
||||
**Registry entry**:
|
||||
|
||||
```bash
|
||||
"auth integrate" "core/shlib/auth-integrate-tty.sh" "continue"
|
||||
```
|
||||
|
||||
**Wrapper requirements** (flow=continue):
|
||||
- Must output result to stdout (usually JSON for structured data)
|
||||
- Exit with proper code (0=success, non-zero=error)
|
||||
|
||||
**Nushell script requirements** (receives flow=continue output):
|
||||
|
||||
```nushell
|
||||
export def "provisioning auth integrate" [--provider: string] {
|
||||
# Check if TTY output exists (guard pattern)
|
||||
let tty_output = ($env.TTY_OUTPUT? | default "")
|
||||
if ($tty_output | is-empty) {
|
||||
error make {msg: "No credentials provided via TTY"}
|
||||
}
|
||||
|
||||
# Parse TTY output (credentials)
|
||||
let credentials = ($tty_output | from json)
|
||||
|
||||
# Use both TTY input ($credentials) and CLI args ($provider)
|
||||
# Complete integration logic...
|
||||
|
||||
# Clear sensitive data after use
|
||||
hide-env TTY_OUTPUT
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Regular Commands
|
||||
|
||||
**Use case**: Standard provisioning operations
|
||||
|
||||
**Example**: `provisioning server list`
|
||||
|
||||
**Flow**:
|
||||
|
||||
```bash
|
||||
Bash wrapper → TTY filter checks registry → Not found → Return 1
|
||||
↓
|
||||
Continue to normal processing:
|
||||
- Fast-path checks (help, workspace, env, etc.)
|
||||
- Daemon check (if applicable)
|
||||
- Nushell dispatcher
|
||||
```
|
||||
|
||||
## Registry Format
|
||||
|
||||
**File**: `provisioning/core/cli/tty-commands.conf`
|
||||
|
||||
**Three-field format**: `"PATTERN" "WRAPPER_PATH" "FLOW_TYPE"`
|
||||
|
||||
```bash
|
||||
# Exact command match (e.g., "setup wizard" matches "provisioning setup wizard")
|
||||
"setup wizard" "core/shlib/setup-wizard-tty.sh" "exit"
|
||||
|
||||
# Paths are relative to $PROVISIONING
|
||||
"auth get-key" "core/shlib/auth-get-key-tty.sh" "pipe"
|
||||
|
||||
# Flow types: exit | pipe | continue
|
||||
"auth integrate" "core/shlib/auth-integrate-tty.sh" "continue"
|
||||
```
|
||||
|
||||
### Flow Type Decision Matrix
|
||||
|
||||
| Interaction | Flow Type | Example |
|
||||
| ----------- | --------- | ------- |
|
||||
| Interactive form, no output needed | `exit` | Setup wizard, auth login |
|
||||
| User input → pipe to next command | `pipe` | API key for piping to deploy |
|
||||
| User input → same-command Nushell processing | `continue` | Credentials for integration |
|
||||
|
||||
## Adding New TTY Commands
|
||||
|
||||
### Step 1: Create Wrapper Script
|
||||
|
||||
Create wrapper in `provisioning/core/shlib/`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
local input
|
||||
|
||||
# Get input from user
|
||||
read -rsp "Prompt: " input
|
||||
echo # Newline
|
||||
|
||||
# For flow=pipe: output to stdout
|
||||
# For flow=continue: output to stdout (will be captured by filter)
|
||||
echo "$input"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
main "$@"
|
||||
```
|
||||
|
||||
Make it executable:
|
||||
|
||||
```bash
|
||||
chmod +x provisioning/core/shlib/your-wrapper-tty.sh
|
||||
```
|
||||
|
||||
### Step 2: Add Registry Entry
|
||||
|
||||
Edit `provisioning/core/cli/tty-commands.conf`:
|
||||
|
||||
```bash
|
||||
# Standalone TTY
|
||||
"your command" "core/shlib/your-wrapper-tty.sh" "exit"
|
||||
|
||||
# Pipeline TTY
|
||||
"get something" "core/shlib/get-something-tty.sh" "pipe"
|
||||
|
||||
# Continue-to-Nushell TTY
|
||||
"setup something" "core/shlib/setup-something-tty.sh" "continue"
|
||||
```
|
||||
|
||||
### Step 3: No Wrapper Modifications Required
|
||||
|
||||
The provisioning wrapper automatically:
|
||||
- Reads registry
|
||||
- Matches command pattern
|
||||
- Routes based on flow type
|
||||
- Handles all three flows
|
||||
|
||||
**No need to modify provisioning wrapper for new commands!**
|
||||
|
||||
## Wrapper Script Requirements
|
||||
|
||||
### For All Wrappers
|
||||
|
||||
- **Shebang**: `#!/bin/bash`
|
||||
- **Safety**: `set -euo pipefail`
|
||||
- **Arguments**: Accept `"${@}"` from wrapper
|
||||
- **Exit codes**: 0=success, non-zero=error
|
||||
- **Validation**: `shellcheck` passes without warnings
|
||||
|
||||
### For flow=exit Wrappers
|
||||
|
||||
- Complete all interaction in wrapper
|
||||
- Exit with proper code (0=success, non-zero=error)
|
||||
- Output shown directly to user (from wrapper)
|
||||
|
||||
### For flow=pipe Wrappers
|
||||
|
||||
- Get input from user (TTY)
|
||||
- Output result to stdout
|
||||
- Output must be newline-terminated
|
||||
- Exit with proper code (0=success, non-zero=error)
|
||||
|
||||
### For flow=continue Wrappers
|
||||
|
||||
- Get input from user (TTY)
|
||||
- Output result to stdout (usually JSON)
|
||||
- Exit with proper code (0=success, non-zero=error)
|
||||
- Filter captures output → $TTY_OUTPUT
|
||||
- Nushell script reads $env.TTY_OUTPUT
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Exported by Filter (flow=continue only)
|
||||
|
||||
- **`$TTY_OUTPUT`**: Captured output from wrapper (available in Nushell as `$env.TTY_OUTPUT`)
|
||||
- **`$PROVISIONING_BYPASS_DAEMON`**: Set to "true" to skip daemon (flow=continue automatically sets this)
|
||||
- **`$TTY_WRAPPER_EXECUTED`**: Set to "true" when TTY wrapper was executed
|
||||
|
||||
### Usage in Nushell
|
||||
|
||||
```nushell
|
||||
# Access TTY output in Nushell script
|
||||
export def "provisioning auth integrate" [--provider: string] {
|
||||
let tty_output = ($env.TTY_OUTPUT? | default "")
|
||||
|
||||
# Parse if JSON
|
||||
let creds = ($tty_output | from json)
|
||||
|
||||
# Use both TTY output and CLI args
|
||||
integration-logic $provider $creds
|
||||
|
||||
# Clear after use (security)
|
||||
hide-env TTY_OUTPUT
|
||||
}
|
||||
```
|
||||
|
||||
## Daemon Interaction
|
||||
|
||||
The flow filter intelligently manages daemon usage:
|
||||
|
||||
### For flow=exit and flow=pipe
|
||||
- ✅ **Daemon can be used** - No stdin required
|
||||
- No output needs to be captured and passed to Nushell
|
||||
- Daemon optimization available (~100ms startup improvement)
|
||||
|
||||
### For flow=continue
|
||||
- ❌ **Daemon MUST be bypassed** - stdin required for TTY interaction
|
||||
- `PROVISIONING_BYPASS_DAEMON=true` automatically set by filter
|
||||
- Direct Nushell execution (preserves stdin for TTY)
|
||||
- Zero overhead (same as non-daemon path)
|
||||
|
||||
## Testing TTY Commands
|
||||
|
||||
### Test Standalone (flow=exit)
|
||||
|
||||
```bash
|
||||
provisioning setup wizard
|
||||
# Expected: TypeDialog form, user interaction, exits
|
||||
```
|
||||
|
||||
### Test Pipeline (flow=pipe)
|
||||
|
||||
```bash
|
||||
provisioning auth get-key | wc -c
|
||||
# Expected: Prompts for API key, outputs to pipe
|
||||
```
|
||||
|
||||
### Test Continue (flow=continue)
|
||||
|
||||
```bash
|
||||
provisioning auth integrate --provider azure
|
||||
# Expected: Prompts for credentials, passes to Nushell with $env.TTY_OUTPUT
|
||||
```
|
||||
|
||||
### Test Regular Command
|
||||
|
||||
```bash
|
||||
provisioning server list
|
||||
# Expected: Normal Nushell processing
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Command Not Executed
|
||||
- **Check**: Is command in tty-commands.conf?
|
||||
- **Check**: Does pattern exactly match command?
|
||||
- **Check**: Is wrapper path correct and executable?
|
||||
|
||||
### Wrapper Not Found
|
||||
- **Error message**: `Warning: TTY wrapper not found or not executable: /path/to/wrapper`
|
||||
- **Check**: File exists at `$PROVISIONING/wrapper-path`
|
||||
- **Check**: File is executable: `chmod +x wrapper-path`
|
||||
|
||||
### Output Not Piping (flow=pipe)
|
||||
- **Check**: Wrapper outputs to stdout (not stderr)
|
||||
- **Check**: Output is newline-terminated: `echo "output"`
|
||||
- **Check**: No daemon interference (PROVISIONING_BYPASS_DAEMON not set)
|
||||
|
||||
### Nushell Not Receiving Output (flow=continue)
|
||||
- **Check**: `$env.TTY_OUTPUT` accessible in Nushell: `echo $env.TTY_OUTPUT`
|
||||
- **Check**: Output format (usually JSON): `echo $env.TTY_OUTPUT | from json`
|
||||
- **Check**: Wrapper exits with 0: `echo $?`
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Filter Location and Function
|
||||
|
||||
**File**: `provisioning/core/cli/tty-filter.sh`
|
||||
**Function**: `filter_tty_command()`
|
||||
**Lines**: ~104 (includes documentation and three flow paths)
|
||||
|
||||
### Integration in Wrapper
|
||||
|
||||
**File**: `provisioning/core/cli/provisioning`
|
||||
**Lines**: ~20 (sources filter, calls function, continues to Nushell)
|
||||
|
||||
### Registry Parsing
|
||||
|
||||
- **File**: `provisioning/core/cli/tty-commands.conf`
|
||||
- **Method**: Line-by-line bash read (no jq dependency)
|
||||
- **Format**: Three-field bash array (bash-compatible)
|
||||
- **Sections**: Organized by flow type for clarity
|
||||
|
||||
## Performance Implications
|
||||
|
||||
### startup time
|
||||
- **flow=exit/pipe**: Daemon available for startup optimization (~100ms improvement)
|
||||
- **flow=continue**: Daemon bypassed (stdin needed), ~500ms traditional path
|
||||
- **Regular commands**: Normal daemon/non-daemon path selection
|
||||
|
||||
### Memory
|
||||
- **flow=continue**: Wrapper output stored in `$TTY_OUTPUT` environment variable
|
||||
- Typical size: < 1KB (credentials, keys, etc.)
|
||||
- Cleared after Nushell processing (or via `hide-env`)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Sensitive Data in $TTY_OUTPUT
|
||||
|
||||
- **Credentials** captured in `$TTY_OUTPUT`
|
||||
- **Nushell scripts should clear after use**: `hide-env TTY_OUTPUT`
|
||||
- **Wrapper output may be logged**: Use standard Unix conventions (hide passwords from output)
|
||||
|
||||
### Wrapper Location Restriction
|
||||
|
||||
- Wrappers should be in `provisioning/core/shlib/` or `provisioning/scripts/`
|
||||
- Registry reads only wrappers from these trusted locations
|
||||
- Pattern validation prevents arbitrary script execution
|
||||
|
||||
### No Shell Injection
|
||||
|
||||
- All variables quoted: `"$variable"`
|
||||
- No eval or command substitution with user input
|
||||
- Pattern matching uses exact string match (no regex)
|
||||
|
||||
## Related Files
|
||||
|
||||
- **Filter**: `provisioning/core/cli/tty-filter.sh`
|
||||
- **Registry**: `provisioning/core/cli/tty-commands.conf`
|
||||
- **Wrapper**: `provisioning/core/cli/provisioning`
|
||||
- **Example wrappers**: `provisioning/core/shlib/auth-get-key-tty.sh`, `provisioning/core/shlib/auth-integrate-tty.sh`
|
||||
|
||||
## Key Insights
|
||||
|
||||
The provisioning wrapper is not just a pass-through - it's a **flow controller** that:
|
||||
|
||||
1. **Detects TTY requirements** (registry matching)
|
||||
2. **Manages execution paths** (three flows: exit, pipe, continue)
|
||||
3. **Controls exit behavior** (standalone vs pipeline vs same-command)
|
||||
4. **Enables inter-command piping** (TTY output to pipes)
|
||||
5. **Supports Nushell integration** (TTY→Nushell continuation)
|
||||
6. **Optimizes with daemon** (skip when stdin needed)
|
||||
|
||||
This solves:
|
||||
- "el tema no es sólo un filter" → ✅ Flow controller with three execution paths
|
||||
- "cómo gestionar el flow por medio del provisioning command" → ✅ Registry + flow types
|
||||
- "usamos tty para input de una API key, se lo pasamos a un script de nushell" → ✅ Pipeline + continue flows
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Last Updated**: January 2026
|
||||
**Status**: ✅ Production Ready
|
||||
130
cli/cache
Executable file
130
cli/cache
Executable file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env nu
|
||||
# Cache management CLI - minimal wrapper for cache operations
|
||||
# Works without requiring an active workspace
|
||||
|
||||
def main [...args: string] {
|
||||
use ../nulib/lib_provisioning/config/cache/simple-cache.nu *
|
||||
|
||||
# Default to "status" if no args
|
||||
let args = if ($args | is-empty) { ["status"] } else { $args }
|
||||
|
||||
# Parse command
|
||||
let command = if ($args | length) > 0 { $args | get 0 } else { "status" }
|
||||
let sub_args = if ($args | length) > 1 { $args | skip 1 } else { [] }
|
||||
|
||||
match $command {
|
||||
"status" => {
|
||||
print ""
|
||||
cache-status
|
||||
print ""
|
||||
}
|
||||
|
||||
"config" => {
|
||||
let sub_cmd = if ($sub_args | length) > 0 { $sub_args | get 0 } else { "show" }
|
||||
match $sub_cmd {
|
||||
"show" => {
|
||||
print ""
|
||||
let config = (get-cache-config)
|
||||
print "Cache Configuration:"
|
||||
print $" enabled: ($config | get --optional enabled | default true)"
|
||||
print $" ttl_final_config: ($config | get --optional ttl_final_config | default 300)s"
|
||||
print $" ttl_kcl: ($config | get --optional ttl_kcl | default 1800)s"
|
||||
print $" ttl_sops: ($config | get --optional ttl_sops | default 900)s"
|
||||
print ""
|
||||
}
|
||||
"get" => {
|
||||
if ($sub_args | length) > 1 {
|
||||
let setting = $sub_args | get 1
|
||||
let value = (cache-config-get $setting)
|
||||
if $value != null {
|
||||
print $"($setting) = ($value)"
|
||||
} else {
|
||||
print $"Setting not found: ($setting)"
|
||||
}
|
||||
} else {
|
||||
print "❌ cache config get requires a setting"
|
||||
print "Usage: cache config get <setting>"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if ($sub_args | length) > 2 {
|
||||
let setting = $sub_args | get 1
|
||||
let value = ($sub_args | skip 2 | str join " ")
|
||||
# Convert value to appropriate type
|
||||
let converted_value = (
|
||||
if $value == "1" or $value == "yes" or $value == "on" { true }
|
||||
else if $value == "0" or $value == "no" or $value == "off" { false }
|
||||
else { $value }
|
||||
)
|
||||
cache-config-set $setting $converted_value
|
||||
# Display the actual value stored
|
||||
let display_value = if $converted_value == true { "true" } else if $converted_value == false { "false" } else { $value }
|
||||
print $"✓ Set ($setting) = ($display_value)"
|
||||
} else {
|
||||
print "❌ cache config set requires setting and value"
|
||||
print "Usage: cache config set <setting> <value>"
|
||||
print " For boolean: use 0/no/off for false, 1/yes/on for true"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown cache config command: ($sub_cmd)"
|
||||
print "Available: show, get, set"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"clear" => {
|
||||
let cache_type = if ($sub_args | length) > 0 { $sub_args | get 0 } else { "all" }
|
||||
cache-clear $cache_type
|
||||
print $"✓ Cleared cache: ($cache_type)"
|
||||
}
|
||||
|
||||
"list" => {
|
||||
let cache_type = if ($sub_args | length) > 0 { $sub_args | get 0 } else { "*" }
|
||||
let items = (cache-list $cache_type)
|
||||
if ($items | length) > 0 {
|
||||
print $"Cache items \(type: ($cache_type)\):"
|
||||
$items | each { |item| print $" ($item)" }
|
||||
} else {
|
||||
print "No cache items found"
|
||||
}
|
||||
}
|
||||
|
||||
"help" | "--help" | "-h" => {
|
||||
print "
|
||||
Cache Management Commands:
|
||||
|
||||
cache status # Show cache status and statistics
|
||||
cache config show # Show cache configuration
|
||||
cache config get <setting> # Get specific cache setting
|
||||
cache config set <setting> <value> # Set cache setting
|
||||
cache clear [type] # Clear cache (default: all)
|
||||
cache list [type] # List cached items (default: all)
|
||||
cache help # Show this help message
|
||||
|
||||
Available settings (for get/set):
|
||||
enabled - Cache enabled (true/false)
|
||||
ttl_final_config - TTL for final config (seconds)
|
||||
ttl_kcl - TTL for KCL compilation (seconds)
|
||||
ttl_sops - TTL for SOPS decryption (seconds)
|
||||
|
||||
Examples:
|
||||
cache status
|
||||
cache config get ttl_final_config
|
||||
cache config set ttl_final_config 600
|
||||
cache config set enabled false
|
||||
cache clear kcl
|
||||
cache list
|
||||
"
|
||||
}
|
||||
|
||||
_ => {
|
||||
print $"❌ Unknown command: ($command)"
|
||||
print "Use 'cache help' for available commands"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
17
cli/cfssl-install.sh
Executable file
17
cli/cfssl-install.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
VERSION="1.6.4"
|
||||
|
||||
# shellcheck disable=SC2006
|
||||
OS=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
|
||||
|
||||
wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssl_${VERSION}_${OS}_${ARCH}
|
||||
if [ -r "cfssl_${VERSION}_${OS}_${ARCH}" ] ; then
|
||||
chmod +x "cfssl_${VERSION}_${OS}_${ARCH}"
|
||||
sudo mv "cfssl_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssl
|
||||
fi
|
||||
wget https://github.com/cloudflare/cfssl/releases/download/v${VERSION}/cfssljson_${VERSION}_${OS}_${ARCH}
|
||||
if [ -r "cfssljson_${VERSION}_${OS}_${ARCH}" ] ; then
|
||||
chmod +x "cfssljson_${VERSION}_${OS}_${ARCH}"
|
||||
sudo mv "cfssljson_${VERSION}_${OS}_${ARCH}" /usr/local/bin/cfssljson
|
||||
fi
|
||||
58
cli/install_config.sh
Executable file
58
cli/install_config.sh
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env bash
|
||||
# Info: Script to install Provisioning config
|
||||
# Author: JesusPerezLorenzo
|
||||
# Release: 1.0.4
|
||||
# Date: 15-04-2024
|
||||
|
||||
|
||||
NU_FILES="
|
||||
core/nulib/libremote.nu
|
||||
core/nulib/lib_provisioning/setup/config.nu
|
||||
"
|
||||
|
||||
WK_FILE=/tmp/make_config_provisioning.nu
|
||||
|
||||
[ -r "$WK_FILE" ] && rm -f "$WK_FILE"
|
||||
|
||||
set -o allexport
|
||||
## shellcheck disable=SC1090
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
set +o allexport
|
||||
|
||||
export NU=$(type -P nu)
|
||||
[ -z "$NU" ] && echo "Nu shell not found" && exit 1
|
||||
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
export PROVISIONING_DEBUG=false
|
||||
|
||||
for it in $NU_FILES
|
||||
do
|
||||
[ -r "$PROVISIONING/$it" ] && cat $PROVISIONING/$it >> $WK_FILE
|
||||
done
|
||||
|
||||
echo "
|
||||
install_config \"reset\" --context
|
||||
" >> $WK_FILE
|
||||
|
||||
NU_ARGS=""
|
||||
CMD_ARGS=""
|
||||
|
||||
DEFAULT_CONTEXT_TEMPLATE="default_context.yaml"
|
||||
case "$(uname | tr '[:upper:]' '[:lower:]')" in
|
||||
linux) PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
;;
|
||||
darwin) PROVISIONING_USER_CONFIG="$HOME/Library/Application\ Support/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/Library/Application\ Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
;;
|
||||
*) PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -d "$PROVISIONING_USER_CONFIG" ] && rm -r "$PROVISIONING_USER_CONFIG"
|
||||
[ -r "$PROVISIONING_CONTEXT_PATH" ] && rm -f "$PROVISIONING_CONTEXT_PATH"
|
||||
|
||||
nu $NU_ARGS $WK_FILE $CMD_ARGS
|
||||
|
||||
rm -f $WK_FILE
|
||||
260
cli/install_nu.sh
Executable file
260
cli/install_nu.sh
Executable file
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env bash
|
||||
# Info: Script to instal NUSHELL for Provisioning
|
||||
# Author: JesusPerezLorenzo
|
||||
# Release: 1.0.5
|
||||
# Date: 8-03-2024
|
||||
|
||||
test_runner() {
|
||||
echo -e "\nTest installation ... "
|
||||
RUNNER_PATH=$(type -P $RUNNER)
|
||||
[ -z "$RUNNER_PATH" ] && echo "🛑 Error $RUNNER not found in PATH ! " && exit 1
|
||||
if $RUNNER ; then
|
||||
echo -e "\n✅ Installation completed successfully ! Use \"$RUNNER\""
|
||||
else
|
||||
echo -e "\n🛑 Error $RUNNER ! Review installation " && exit 1
|
||||
fi
|
||||
}
|
||||
register_plugins() {
|
||||
local source=$1
|
||||
local warn=$2
|
||||
[ ! -d "$source" ] && echo "🛑 Error path $source is not a directory" && exit 1
|
||||
[ -z "$(ls $source/nu_plugin_* 2> /dev/null)" ] && echo "🛑 Error no 'nu_plugin_*' found in $source to register" && exit 1
|
||||
echo -e "Nushell $NU_VERSION plugins registration \n"
|
||||
if [ -n "$warn" ] ; then
|
||||
echo -e $"❗Warning: Be sure Nushell plugins are compiled for same Nushell version $NU_VERSION\n otherwise will probably not work and will break installation !\n"
|
||||
fi
|
||||
for plugin in ${source}/nu_plugin_*
|
||||
do
|
||||
if $source/nu -c "register \"${plugin}\" " 2>/dev/null ; then
|
||||
echo -en "$(basename $plugin)"
|
||||
if [[ "$plugin" == *_notifications ]] ; then
|
||||
echo -e " registred "
|
||||
else
|
||||
echo -e "\t\t registred "
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Install nu_plugin_tera if available
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
echo -e "Installing nu_plugin_tera..."
|
||||
if cargo install nu_plugin_tera; then
|
||||
if $source/nu -c "register ~/.cargo/bin/nu_plugin_tera" 2>/dev/null; then
|
||||
echo -e "nu_plugin_tera\t\t registred"
|
||||
else
|
||||
echo -e "❗ Failed to register nu_plugin_tera"
|
||||
fi
|
||||
else
|
||||
echo -e "❗ Failed to install nu_plugin_tera"
|
||||
fi
|
||||
else
|
||||
echo -e "❗ Cargo not found - nu_plugin_tera not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check Nickel configuration language installation
|
||||
check_nickel_installation() {
|
||||
if command -v nickel >/dev/null 2>&1; then
|
||||
nickel_version=$(nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
echo -e "Nickel\t\t\t already installed (version $nickel_version)"
|
||||
return 0
|
||||
else
|
||||
echo -e "⚠️ Nickel not found - Optional but recommended for config rendering"
|
||||
echo -e " Install via: \$PROVISIONING/core/cli/tools-install nickel"
|
||||
echo -e " Recommended method: nix profile install nixpkgs#nickel"
|
||||
echo -e " (Pre-built binaries have Nix library dependencies)"
|
||||
echo -e " https://nickel-lang.org/getting-started"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_mode() {
|
||||
local mode=$1
|
||||
case "$mode" in
|
||||
ui| desktop)
|
||||
if cp $PROVISIONING_MODELS_SRC/plugins_defs.nu $PROVISIONING_MODELS_TARGET/plugins_defs.nu ; then
|
||||
echo "Mode $mode installed"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
NC_PATH=$(type -P nc)
|
||||
if [ -z "$NC_PATH" ] ; then
|
||||
echo "'nc' command not found in PATH. Install 'nc' (netcat) command."
|
||||
exit 1
|
||||
fi
|
||||
if cp $PROVISIONING_MODELS_SRC/no_plugins_defs.nu $PROVISIONING_MODELS_TARGET/plugins_defs.nu ; then
|
||||
echo "Mode 'no plugins' installed"
|
||||
fi
|
||||
esac
|
||||
}
|
||||
install_from_url() {
|
||||
local target_path=$1
|
||||
local lib_mode
|
||||
local url_source
|
||||
local download_path
|
||||
local download_url
|
||||
local tar_file
|
||||
|
||||
[ ! -d "$target_path" ] && echo "🛑 Error path $target_path is not a directory" && exit 1
|
||||
lib_mode=$(grep NU_LIB $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g')
|
||||
url_source=$(grep NU_SOURCE $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g')
|
||||
download_path="nu-${NU_VERSION}-${ARCH_ORG}-${OS}"
|
||||
case "$OS" in
|
||||
linux) download_path="nu-${NU_VERSION}-${ARCH_ORG}-unknown-${OS}-gnu"
|
||||
;;
|
||||
esac
|
||||
download_url="$url_source/${NU_VERSION}/$download_path.tar.gz"
|
||||
tar_file=$download_path.tar.gz
|
||||
echo -e "Nushell $NU_VERSION downloading ..."
|
||||
if ! curl -sSfL $download_url -o $tar_file ; then
|
||||
echo "🛑 Error download $download_url " && exit 1
|
||||
return 1
|
||||
fi
|
||||
echo -e "Nushell $NU_VERSION extracting ..."
|
||||
if ! tar xzf $tar_file ; then
|
||||
echo "🛑 Error download $download_url " && exit 1
|
||||
return 1
|
||||
fi
|
||||
rm -f $tar_file
|
||||
if [ ! -d "$download_path" ] ; then
|
||||
echo "🛑 Error $download_path not found " && exit 1
|
||||
return 1
|
||||
fi
|
||||
echo -e "Nushell $NU_VERSION installing ..."
|
||||
if [ -r "$download_path/nu" ] ; then
|
||||
chmod +x $download_path/nu
|
||||
if ! sudo cp $download_path/nu $target_path ; then
|
||||
echo "🛑 Error installing \"nu\" in $target_path"
|
||||
rm -rf $download_path
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
rm -rf $download_path
|
||||
echo "✅ Nushell and installed in $target_path"
|
||||
[[ ! "$PATH" =~ $target_path ]] && echo "❗ Warning: \"$target_path\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings "
|
||||
echo ""
|
||||
# TDOO install plguins via cargo ??
|
||||
# TODO a NU version without PLUGINS
|
||||
# register_plugins $target_path
|
||||
}
|
||||
|
||||
install_from_local() {
|
||||
local source=$1
|
||||
local target=$2
|
||||
local tmpdir
|
||||
|
||||
[ ! -d "$target" ] && echo "🛑 Error path $target is not a directory" && exit 1
|
||||
[ ! -r "$source/nu.gz" ] && echo "🛑 Error command 'nu' not found in $source/nu.gz" && exit 1
|
||||
|
||||
echo -e "Nushell $NU_VERSION self installation guarantees consistency with plugins and settings \n"
|
||||
tmpdir=$(mktemp -d)
|
||||
cp $source/*gz $tmpdir
|
||||
for file in $tmpdir/*gz ; do gunzip $file ; done
|
||||
if ! sudo mv $tmpdir/* $target ; then
|
||||
echo -e "🛑 Errors to install Nushell and plugins in \"${target}\""
|
||||
rm -rf $tmpdir
|
||||
return 1
|
||||
fi
|
||||
rm -rf $tmpdir
|
||||
echo "✅ Nushell and plugins installed in $target"
|
||||
[[ ! "$PATH" =~ $target ]] && echo "❗ Warning: \"$target\" is not in your PATH for $(basename $SHELL) ! Fix your PATH settings "
|
||||
echo ""
|
||||
register_plugins $target
|
||||
}
|
||||
|
||||
message_install() {
|
||||
local ask=$1
|
||||
local msg
|
||||
local answer
|
||||
[ -r "$PROVISIONING/resources/ascii.txt" ] && cat "$PROVISIONING/resources/ascii.txt" && echo ""
|
||||
if [ -z "$NU" ] ; then
|
||||
echo -e "🛑 Nushell $NU_VERSION not installed is mandatory for \"${RUNNER}\""
|
||||
echo -e "Check PATH or https://www.nushell.sh/book/installation.html with version $NU_VERSION"
|
||||
else
|
||||
echo -e "Nushell $NU_VERSION update for \"${RUNNER}\""
|
||||
fi
|
||||
echo ""
|
||||
if [ -n "$ask" ] && [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then
|
||||
echo -en "Install Nushell $(uname -m) $(uname) in \"$INSTALL_PATH\" now (yes/no) ? : "
|
||||
read -r answer
|
||||
if [ "$answer" != "yes" ] && [ "$answer" != "y" ] ; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ -d "$(dirname $0)/nu/${ARCH}-${OS}" ] ; then
|
||||
install_from_local $(dirname $0)/nu/${ARCH}-${OS} $INSTALL_PATH
|
||||
install_mode "ui"
|
||||
else
|
||||
install_from_url $INSTALL_PATH
|
||||
install_mode ""
|
||||
fi
|
||||
echo ""
|
||||
echo -e "Checking optional configuration languages..."
|
||||
check_nickel_installation
|
||||
}
|
||||
|
||||
set +o errexit
|
||||
set +o pipefail
|
||||
|
||||
RUNNER="provisioning"
|
||||
export NU=$(type -P nu)
|
||||
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
[ -r "../env-provisioning" ] && source ../env-provisioning
|
||||
[ -r "env-provisioning" ] && source ./env-provisioning
|
||||
#[ -r ".env" ] && source .env set
|
||||
set +o allexport
|
||||
|
||||
if [ -n "$1" ] && [ -d "$1" ] && [ -d "$1/core" ] ; then
|
||||
export PROVISIONING=$1
|
||||
else
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
fi
|
||||
|
||||
TASK=${1:-check}
|
||||
shift
|
||||
if [ "$TASK" == "mode" ] && [ -n "$1" ] ; then
|
||||
INSTALL_MODE=$1
|
||||
shift
|
||||
else
|
||||
INSTALL_MODE="ui"
|
||||
fi
|
||||
|
||||
ASK_MESSAGE="ask"
|
||||
[ -n "$1" ] && [ "$1" == "no-ask" ] && ASK_MESSAGE="" && shift
|
||||
[ -n "$1" ] && [ "$1" == "mode-ui" ] && INSTALL_MODE="ui" && shift
|
||||
[ -n "$1" ] && [[ "$1" == mode-* ]] && INSTALL_MODE="" && shift
|
||||
|
||||
INSTALL_PATH=${1:-/usr/local/bin}
|
||||
|
||||
NU_VERSION=$(grep NU_VERSION $PROVISIONING/core/versions | cut -f2 -d"=" | sed 's/"//g')
|
||||
#ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
|
||||
ARCH="$(uname -m | sed -e 's/amd64/x86_64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
|
||||
ARCH_ORG="$(uname -m | tr '[:upper:]' '[:lower:]')"
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
PROVISIONING_MODELS_SRC=$PROVISIONING/core/nulib/models
|
||||
PROVISIONING_MODELS_TARGET=$PROVISIONING/core/nulib/lib_provisioning
|
||||
|
||||
USAGE="$(basename $0) [install | reinstall | mode | check] no-ask mode-?? "
|
||||
case $TASK in
|
||||
install)
|
||||
message_install $ASK_MESSAGE
|
||||
;;
|
||||
reinstall | update)
|
||||
INSTALL_PATH=$(dirname $NU)
|
||||
if message_install ; then
|
||||
test_runner
|
||||
fi
|
||||
;;
|
||||
mode)
|
||||
install_mode $INSTALL_MODE
|
||||
;;
|
||||
check)
|
||||
$PROVISIONING/core/bin/tools-install check nu
|
||||
;;
|
||||
help|-h)
|
||||
echo "$USAGE"
|
||||
;;
|
||||
*) echo "Option $TASK not defined"
|
||||
esac
|
||||
981
cli/module-loader
Executable file
981
cli/module-loader
Executable file
|
|
@ -0,0 +1,981 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# Enhanced Module Loader CLI
|
||||
# Unified CLI for discovering and loading taskservs, providers, and clusters
|
||||
# Includes template and layer support from enhanced version
|
||||
|
||||
use ../nulib/taskservs/discover.nu *
|
||||
use ../nulib/taskservs/load.nu *
|
||||
use ../nulib/providers/discover.nu *
|
||||
use ../nulib/providers/load.nu *
|
||||
use ../nulib/clusters/discover.nu *
|
||||
use ../nulib/clusters/load.nu *
|
||||
use ../nulib/lib_provisioning/module_loader.nu *
|
||||
use ../nulib/lib_provisioning/config/accessor.nu config-get
|
||||
|
||||
# Main module loader command with enhanced features
|
||||
def main [subcommand?: string] {
|
||||
if ($subcommand | is-empty) {
|
||||
print_enhanced_help
|
||||
return
|
||||
}
|
||||
|
||||
match $subcommand {
|
||||
"help" => print_enhanced_help
|
||||
"discover" => print_discover_help
|
||||
"load" => print_load_help
|
||||
"list" => print_list_help
|
||||
"unload" => print_unload_help
|
||||
"init" => print_init_help
|
||||
"validate" => print_validate_help
|
||||
"info" => print_info_help
|
||||
"template" => print_template_help
|
||||
"layer" => print_layer_help
|
||||
"override" => print_override_help
|
||||
_ => {
|
||||
print $"Unknown command: ($subcommand)"
|
||||
print_enhanced_help
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# === DISCOVERY COMMANDS ===
|
||||
|
||||
# Discover available modules
|
||||
export def "main discover" [
|
||||
type: string, # Module type: taskservs, providers, clusters
|
||||
query?: string, # Search query
|
||||
--format: string = "table", # Output format: table, yaml, json, names
|
||||
--category: string = "", # Filter by category (for taskservs)
|
||||
--group: string = "" # Filter by group (for taskservs)
|
||||
] {
|
||||
match $type {
|
||||
"taskservs" => {
|
||||
let taskservs = if ($query | is-empty) {
|
||||
discover-taskservs
|
||||
} else {
|
||||
search-taskservs $query
|
||||
}
|
||||
|
||||
let filtered = if ($category | is-empty) and ($group | is-empty) {
|
||||
$taskservs
|
||||
} else if not ($category | is-empty) {
|
||||
$taskservs | where group == $category
|
||||
} else if not ($group | is-empty) {
|
||||
$taskservs | where group == $group
|
||||
} else {
|
||||
$taskservs
|
||||
}
|
||||
|
||||
format_output $filtered $format
|
||||
}
|
||||
"providers" => {
|
||||
print "Provider discovery not implemented yet"
|
||||
}
|
||||
"clusters" => {
|
||||
print "Cluster discovery not implemented yet"
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown module type: ($type)"
|
||||
print "Available types: taskservs, providers, clusters"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Sync Nickel dependencies for infrastructure workspace
|
||||
export def "main sync" [
|
||||
infra: string, # Infrastructure name or path
|
||||
--manifest: string = "providers.manifest.yaml", # Manifest file name
|
||||
--show-modules # Show module info after sync
|
||||
] {
|
||||
# Resolve infrastructure path
|
||||
let infra_path = if ($infra | path exists) {
|
||||
$infra
|
||||
} else {
|
||||
# Try workspace path
|
||||
let workspace_path = $"workspace/infra/($infra)"
|
||||
if ($workspace_path | path exists) {
|
||||
$workspace_path
|
||||
} else {
|
||||
print $"❌ Infrastructure not found: ($infra)"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Sync Nickel dependencies using library function
|
||||
sync-nickel-dependencies $infra_path --manifest $manifest
|
||||
|
||||
# Show Nickel module info if requested
|
||||
if $show_modules {
|
||||
print ""
|
||||
print "📋 Nickel Modules:"
|
||||
let modules_dir = (get-config-value "nickel" "modules_dir")
|
||||
let modules_path = ($infra_path | path join $modules_dir)
|
||||
|
||||
if ($modules_path | path exists) {
|
||||
ls $modules_path | each {|entry|
|
||||
print $" • ($entry.name | path basename) → ($entry.name)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# === LOAD/UNLOAD COMMANDS ===
|
||||
|
||||
# Load modules into workspace
|
||||
export def "main load" [
|
||||
type: string, # Module type: taskservs, providers, clusters
|
||||
workspace: string, # Workspace path
|
||||
...modules: string, # Module names to load
|
||||
--layer: string = "workspace", # Layer to load into: workspace, infra
|
||||
--validate # Validate after loading
|
||||
--force (-f) # Force overwrite existing files
|
||||
] {
|
||||
if ($modules | is-empty) {
|
||||
print $"No modules specified for loading"
|
||||
return
|
||||
}
|
||||
|
||||
print $"Loading ($modules | length) ($type) into ($workspace) at layer ($layer)"
|
||||
|
||||
match $type {
|
||||
"taskservs" | "providers" | "clusters" | "workflows" => {
|
||||
load_extension_to_workspace $type $workspace $modules $layer $force
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown module type: ($type)"
|
||||
}
|
||||
}
|
||||
|
||||
if $validate {
|
||||
main validate $workspace
|
||||
}
|
||||
}
|
||||
|
||||
# Enhanced load with template support
|
||||
export def "main load enhanced" [
|
||||
type: string, # Module type
|
||||
workspace: string, # Workspace path
|
||||
infra: string, # Infrastructure name
|
||||
modules: list<string>, # Module names
|
||||
--layer: string = "workspace", # Target layer
|
||||
--template-base # Use template as base
|
||||
] {
|
||||
print $"🚀 Enhanced loading ($modules | length) ($type) for infra ($infra)"
|
||||
|
||||
for module in $modules {
|
||||
print $" 📦 Loading ($module)..."
|
||||
|
||||
# Check if template exists for this module
|
||||
let template_path = $"provisioning/workspace/templates/taskservs/*/($module).k"
|
||||
let has_template = (glob $template_path | length) > 0
|
||||
|
||||
if $has_template and $template_base {
|
||||
print $" ✓ Using template base for ($module)"
|
||||
# Template-based loading would go here
|
||||
} else {
|
||||
print $" ✓ Direct loading for ($module)"
|
||||
# Direct loading
|
||||
}
|
||||
}
|
||||
|
||||
print "✅ Enhanced loading completed"
|
||||
}
|
||||
|
||||
# Unload module from workspace
|
||||
export def "main unload" [
|
||||
type: string, # Module type
|
||||
workspace: string, # Workspace path
|
||||
module: string, # Module name to unload
|
||||
--layer: string = "workspace" # Layer to unload from
|
||||
] {
|
||||
print $"Unloading ($module) from ($workspace) at layer ($layer)"
|
||||
|
||||
match $type {
|
||||
"taskservs" => {
|
||||
unload_taskserv_from_workspace $workspace $module $layer
|
||||
}
|
||||
"providers" => {
|
||||
print "Provider unloading not implemented yet"
|
||||
}
|
||||
"clusters" => {
|
||||
print "Cluster unloading not implemented yet"
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown module type: ($type)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# === LIST COMMANDS ===
|
||||
|
||||
# List modules in workspace
|
||||
export def "main list" [
|
||||
type: string, # Module type
|
||||
workspace: string, # Workspace path
|
||||
--layer: string = "all", # Layer to list: workspace, infra, all
|
||||
--format: string = "table" # Output format
|
||||
] {
|
||||
print $"Listing ($type) in ($workspace) for layer ($layer)"
|
||||
|
||||
match $type {
|
||||
"taskservs" => {
|
||||
list_workspace_taskservs $workspace $layer $format
|
||||
}
|
||||
"providers" => {
|
||||
print "Provider listing not implemented yet"
|
||||
}
|
||||
"clusters" => {
|
||||
print "Cluster listing not implemented yet"
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown module type: ($type)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# === TEMPLATE COMMANDS ===
|
||||
|
||||
# List available templates
|
||||
export def "main template list" [
|
||||
--template-type: string = "all", # Template type: taskservs, providers, servers, clusters
|
||||
--format: string = "table" # Output format
|
||||
] {
|
||||
print $"📋 Available templates type: ($template_type)"
|
||||
|
||||
let template_base = "provisioning/workspace/templates"
|
||||
|
||||
match $template_type {
|
||||
"taskservs" | "all" => {
|
||||
let taskserv_templates = if (($template_base | path join "taskservs") | path exists) {
|
||||
glob ($template_base | path join "taskservs" "*" "*.k")
|
||||
| each { |path|
|
||||
let category = ($path | path dirname | path basename)
|
||||
let name = ($path | path basename | str replace ".k" "")
|
||||
{ type: "taskserv", category: $category, name: $name, path: $path }
|
||||
}
|
||||
} else { [] }
|
||||
|
||||
format_output $taskserv_templates $format
|
||||
}
|
||||
"providers" => {
|
||||
print "Provider templates not implemented yet"
|
||||
}
|
||||
"servers" => {
|
||||
let server_templates = if (($template_base | path join "servers") | path exists) {
|
||||
ls ($template_base | path join "servers") | get name
|
||||
| each { |path| { type: "server", name: ($path | path basename), path: $path } }
|
||||
} else { [] }
|
||||
|
||||
format_output $server_templates $format
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown template type: ($template_type)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Extract template from existing infrastructure
|
||||
export def "main template extract" [
|
||||
source_infra: string, # Source infrastructure path
|
||||
template_name: string, # Name for the new template
|
||||
--type: string = "taskserv", # Template type
|
||||
--output: string = "provisioning/workspace/templates" # Output directory
|
||||
] {
|
||||
print $"📤 Extracting template ($template_name) from ($source_infra)"
|
||||
|
||||
# Implementation would analyze the source infra and create template
|
||||
print "Template extraction not yet implemented"
|
||||
}
|
||||
|
||||
# Apply template to infrastructure
|
||||
export def "main template apply" [
|
||||
template_name: string, # Template to apply
|
||||
target_infra: string, # Target infrastructure
|
||||
--override-file: string = "", # Override file path
|
||||
--dry-run # Show what would be done
|
||||
] {
|
||||
if $dry_run {
|
||||
print $"🔍 [DRY RUN] Would apply template ($template_name) to ($target_infra)"
|
||||
} else {
|
||||
print $"📥 Applying template ($template_name) to ($target_infra)"
|
||||
}
|
||||
|
||||
# Implementation would apply template with overrides
|
||||
print "Template application not yet implemented"
|
||||
}
|
||||
|
||||
# === LAYER COMMANDS ===
|
||||
|
||||
# Show layer information
|
||||
export def "main layer show" [
|
||||
workspace: string, # Workspace path
|
||||
--module: string = "", # Specific module to show
|
||||
--type: string = "taskservs" # Module type
|
||||
] {
|
||||
print $"📊 Layer information for ($workspace)"
|
||||
|
||||
if not ($module | is-empty) {
|
||||
# Use existing layer utilities
|
||||
try {
|
||||
nu -c $"use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution ($module) ($workspace) upcloud"
|
||||
} catch {
|
||||
print $"Could not test layer resolution for ($module)"
|
||||
}
|
||||
} else {
|
||||
print "Showing overall layer structure..."
|
||||
try {
|
||||
nu -c "use provisioning/workspace/tools/layer-utils.nu *; show_layer_stats"
|
||||
} catch {
|
||||
print "Could not show layer statistics"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Test layer resolution
|
||||
export def "main layer test" [
|
||||
module: string, # Module to test
|
||||
workspace: string, # Workspace/infra name
|
||||
provider: string = "upcloud" # Provider for testing
|
||||
] {
|
||||
print $"🧪 Testing layer resolution: ($module) in ($workspace) with ($provider)"
|
||||
|
||||
try {
|
||||
nu -c $"use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution ($module) ($workspace) ($provider)"
|
||||
} catch {
|
||||
print $"❌ Layer resolution test failed for ($module)"
|
||||
}
|
||||
}
|
||||
|
||||
# === OVERRIDE COMMANDS ===
|
||||
|
||||
# Create configuration override
|
||||
export def "main override create" [
|
||||
type: string, # Type: taskservs, providers, clusters
|
||||
infra: string, # Infrastructure name
|
||||
module: string, # Module name
|
||||
--from: string = "", # Template to base override on
|
||||
--layer: string = "infra" # Layer for override
|
||||
] {
|
||||
print $"⚙️ Creating override for ($module) in ($infra) at layer ($layer)"
|
||||
|
||||
let override_path = match $layer {
|
||||
"infra" => $"workspace/infra/($infra)/overrides/($module).k"
|
||||
"workspace" => $"provisioning/workspace/templates/($type)/($module).k"
|
||||
_ => {
|
||||
print $"Unknown layer: ($layer)"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
print $"📝 Override will be created at: ($override_path)"
|
||||
|
||||
if not ($from | is-empty) {
|
||||
print $"📋 Based on template: ($from)"
|
||||
}
|
||||
|
||||
# Create directory if needed
|
||||
mkdir ($override_path | path dirname)
|
||||
|
||||
# Create basic override file
|
||||
let content = if not ($from | is-empty) {
|
||||
$"# Override for ($module) in ($infra)
|
||||
# Based on template: ($from)
|
||||
|
||||
import ($type).*.($module).ncl.($module) as base
|
||||
import provisioning.workspace.templates.($type).($from) as template
|
||||
|
||||
# Infrastructure-specific overrides
|
||||
($module)_($infra)_override: base.($module | str capitalize) = template.($from)_template {
|
||||
# Add your overrides here
|
||||
# Example:
|
||||
# replicas = 3
|
||||
# resources.memory = \"1Gi\"
|
||||
}
|
||||
"
|
||||
} else {
|
||||
$"# Override for ($module) in ($infra)
|
||||
|
||||
import ($type).*.($module).ncl.($module) as base
|
||||
|
||||
# Infrastructure-specific overrides
|
||||
($module)_($infra)_override: base.($module | str capitalize) = base.($module)_config {
|
||||
# Add your overrides here
|
||||
# Example:
|
||||
# replicas = 3
|
||||
# resources.memory = \"1Gi\"
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
$content | save $override_path
|
||||
print $"✅ Override created: ($override_path)"
|
||||
}
|
||||
|
||||
# === WORKSPACE MANAGEMENT ===
|
||||
|
||||
# Initialize workspace with modules
|
||||
export def "main init" [
|
||||
workspace: string, # Workspace path
|
||||
--modules: list<string> = [], # Initial modules to load
|
||||
--template: string = "", # Workspace template
|
||||
--provider: string = "upcloud" # Default provider
|
||||
] {
|
||||
print $"🚀 Initializing workspace: ($workspace)"
|
||||
|
||||
# Create workspace structure
|
||||
let workspace_dirs = [
|
||||
$"($workspace)/config"
|
||||
$"($workspace)/taskservs"
|
||||
$"($workspace)/overrides"
|
||||
$"($workspace)/defs"
|
||||
$"($workspace)/clusters"
|
||||
]
|
||||
|
||||
for dir in $workspace_dirs {
|
||||
mkdir $dir
|
||||
print $" 📁 Created: ($dir)"
|
||||
}
|
||||
|
||||
# Create basic configuration
|
||||
let config_content = $"# Workspace configuration for ($workspace)
|
||||
# Provider: ($provider)
|
||||
# Initialized: (date now)
|
||||
|
||||
provider = "($provider)"
|
||||
workspace = "($workspace)"
|
||||
"
|
||||
$config_content | save $"($workspace)/config/workspace.toml"
|
||||
print $" 📄 Created: ($workspace)/config/workspace.toml"
|
||||
|
||||
# Load initial modules
|
||||
if ($modules | length) > 0 {
|
||||
print $"📦 Loading initial modules: (($modules | str join ', '))"
|
||||
main load taskservs $workspace ...$modules
|
||||
}
|
||||
|
||||
print $"✅ Workspace ($workspace) initialized successfully"
|
||||
}
|
||||
|
||||
# Validate workspace integrity
|
||||
export def "main validate" [workspace: string] {
|
||||
print $"🔍 Validating workspace: ($workspace)"
|
||||
|
||||
let required_dirs = ["config", "taskservs", "overrides", "defs"]
|
||||
mut validation_errors = []
|
||||
|
||||
for dir in $required_dirs {
|
||||
let full_path = ($workspace | path join $dir)
|
||||
if not ($full_path | path exists) {
|
||||
$validation_errors = ($validation_errors | append $"Missing directory: ($full_path)")
|
||||
}
|
||||
}
|
||||
|
||||
# Check configuration file
|
||||
let config_file = ($workspace | path join "config" "workspace.toml")
|
||||
if not ($config_file | path exists) {
|
||||
$validation_errors = ($validation_errors | append $"Missing configuration: ($config_file)")
|
||||
}
|
||||
|
||||
# Report results
|
||||
if ($validation_errors | is-empty) {
|
||||
print "✅ Workspace validation passed"
|
||||
return true
|
||||
} else {
|
||||
print "❌ Workspace validation failed:"
|
||||
for error in $validation_errors {
|
||||
print $" • ($error)"
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
# Show workspace information
|
||||
export def "main info" [workspace: string] {
|
||||
print $"📊 Workspace Information: ($workspace)"
|
||||
|
||||
if not (($workspace | path join "config" "workspace.toml") | path exists) {
|
||||
print "❌ Workspace not found or not initialized"
|
||||
return
|
||||
}
|
||||
|
||||
# Show basic info
|
||||
let config = try { open ($workspace | path join "config" "workspace.toml") | from toml } catch { {} }
|
||||
|
||||
print $" Provider: (($config.provider? | default 'unknown'))"
|
||||
print $" Path: ($workspace)"
|
||||
|
||||
# Count modules
|
||||
let taskserv_count = try {
|
||||
ls ($workspace | path join "taskservs") | length
|
||||
} catch { 0 }
|
||||
|
||||
let override_count = try {
|
||||
ls ($workspace | path join "overrides") | length
|
||||
} catch { 0 }
|
||||
|
||||
print $" Task Services: ($taskserv_count)"
|
||||
print $" Overrides: ($override_count)"
|
||||
|
||||
# Show recent activity
|
||||
let recent_files = try {
|
||||
ls $workspace | where type == file | sort-by modified | last 3 | get name
|
||||
} catch { [] }
|
||||
|
||||
if ($recent_files | length) > 0 {
|
||||
print " Recent activity:"
|
||||
for file in $recent_files {
|
||||
print $" • ($file | path basename)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# === HELPER FUNCTIONS ===
|
||||
|
||||
# Generic extension loading function (taskservs, providers, clusters, workflows)
|
||||
def load_extension_to_workspace [
|
||||
extension_type: string, # taskservs, providers, clusters, workflows
|
||||
workspace: string,
|
||||
modules: list<string>,
|
||||
layer: string,
|
||||
force: bool = false
|
||||
] {
|
||||
# Get extension-specific info function based on type
|
||||
let get_info_fn = match $extension_type {
|
||||
"taskservs" => { |name| get-taskserv-info $name }
|
||||
"providers" => { |name| get-provider-info $name }
|
||||
"clusters" => { |name| get-cluster-info $name }
|
||||
_ => { |name| {name: $name, group: "", type: $extension_type} }
|
||||
}
|
||||
|
||||
# Get source path from config
|
||||
let source_base_path = (config-get $"paths.($extension_type)" | path expand)
|
||||
|
||||
# Get template base path from config
|
||||
let provisioning_base = (config-get "paths.base" | path expand)
|
||||
let template_base_path = ($provisioning_base | path join "workspace" "templates" $extension_type)
|
||||
|
||||
for module in $modules {
|
||||
print $" 📦 Loading ($extension_type): ($module)"
|
||||
|
||||
# Get module info
|
||||
let module_info = try {
|
||||
do $get_info_fn $module
|
||||
} catch {
|
||||
print $" ❌ Module not found: ($module)"
|
||||
continue
|
||||
}
|
||||
|
||||
print $" ✓ Found: ($module_info.name) (($module_info.group? | default ""))"
|
||||
|
||||
# Resolve workspace paths
|
||||
let workspace_abs = ($workspace | path expand)
|
||||
let workspace_root = if ($workspace_abs | str contains "/infra/") {
|
||||
let parts = ($workspace_abs | split row "/infra/")
|
||||
$parts.0
|
||||
} else {
|
||||
$workspace_abs
|
||||
}
|
||||
|
||||
# Build source path (handle optional group, "root" means no category)
|
||||
let group_path = ($module_info.group? | default "")
|
||||
let group_path = if ($group_path == "root") { "" } else { $group_path }
|
||||
let source_module_path = if ($group_path | is-not-empty) {
|
||||
$source_base_path | path join $group_path $module
|
||||
} else {
|
||||
$source_base_path | path join $module
|
||||
}
|
||||
|
||||
# STEP 1: Copy schemas to workspace/.{extension_type}
|
||||
let target_schemas_dir = ($workspace_root | path join $".($extension_type)")
|
||||
let target_module_path = if ($group_path | is-not-empty) {
|
||||
$target_schemas_dir | path join $group_path $module
|
||||
} else {
|
||||
$target_schemas_dir | path join $module
|
||||
}
|
||||
|
||||
# Config file directory
|
||||
let config_dir = ($workspace_abs | path join $extension_type)
|
||||
let config_file_path = ($config_dir | path join $"($module).k")
|
||||
|
||||
# Check if already loaded
|
||||
if ($config_file_path | path exists) and ($target_module_path | path exists) {
|
||||
if not $force {
|
||||
print $" ✅ Module already loaded: ($module)"
|
||||
print $" Config: ($config_file_path)"
|
||||
print $" Source: ($target_module_path)"
|
||||
print $" 💡 Use --force to overwrite existing files"
|
||||
continue
|
||||
} else {
|
||||
print $" 🔄 Overwriting existing module: ($module)"
|
||||
}
|
||||
}
|
||||
|
||||
# Copy schemas from system extensions to workspace
|
||||
let parent_dir = ($target_module_path | path dirname)
|
||||
mkdir $parent_dir
|
||||
|
||||
if ($source_module_path | path exists) {
|
||||
print $" 📦 Copying schemas to workspace .($extension_type)..."
|
||||
print $" From: ($source_module_path)"
|
||||
print $" To: ($target_module_path)"
|
||||
|
||||
if ($target_module_path | path exists) {
|
||||
rm -rf $target_module_path
|
||||
}
|
||||
|
||||
cp -r $source_module_path $parent_dir
|
||||
print $" ✓ Schemas copied to workspace .($extension_type)/"
|
||||
|
||||
# STEP 2a: Update individual module's nickel.mod with correct workspace paths
|
||||
# Calculate relative paths based on categorization depth
|
||||
let provisioning_path = if ($group_path | is-not-empty) {
|
||||
# Categorized: .{ext}/{category}/{module}/nickel/ -> ../../../../.nickel/packages/provisioning
|
||||
"../../../../.nickel/packages/provisioning"
|
||||
} else {
|
||||
# Non-categorized: .{ext}/{module}/nickel/ -> ../../../.nickel/packages/provisioning
|
||||
"../../../.nickel/packages/provisioning"
|
||||
}
|
||||
|
||||
let parent_path = if ($group_path | is-not-empty) {
|
||||
# Categorized: .{ext}/{category}/{module}/nickel/ -> ../../..
|
||||
"../../.."
|
||||
} else {
|
||||
# Non-categorized: .{ext}/{module}/nickel/ -> ../..
|
||||
"../.."
|
||||
}
|
||||
|
||||
# Update the module's nickel.mod file with workspace-relative paths
|
||||
let module_nickel_mod_path = ($target_module_path | path join "nickel" "nickel.mod")
|
||||
if ($module_nickel_mod_path | path exists) {
|
||||
print $" 🔧 Updating module nickel.mod with workspace paths"
|
||||
let module_nickel_mod_content = $"[package]
|
||||
name = \"($module)\"
|
||||
edition = \"v0.11.3\"
|
||||
version = \"0.0.1\"
|
||||
|
||||
[dependencies]
|
||||
provisioning = { path = \"($provisioning_path)\", version = \"0.0.1\" }
|
||||
($extension_type) = { path = \"($parent_path)\", version = \"0.1.0\" }
|
||||
"
|
||||
$module_nickel_mod_content | save -f $module_nickel_mod_path
|
||||
print $" ✓ Updated nickel.mod: ($module_nickel_mod_path)"
|
||||
}
|
||||
} else {
|
||||
print $" ⚠️ Warning: Source not found at ($source_module_path)"
|
||||
}
|
||||
|
||||
# STEP 2b: Create nickel.mod in workspace/.{extension_type}
|
||||
let extension_nickel_mod = ($target_schemas_dir | path join "nickel.mod")
|
||||
if not ($extension_nickel_mod | path exists) {
|
||||
print $" 📦 Creating nickel.mod for .($extension_type) package"
|
||||
let nickel_mod_content = $"[package]
|
||||
name = \"($extension_type)\"
|
||||
edition = \"v0.11.3\"
|
||||
version = \"0.1.0\"
|
||||
description = \"Workspace-level ($extension_type) schemas\"
|
||||
"
|
||||
$nickel_mod_content | save $extension_nickel_mod
|
||||
}
|
||||
|
||||
# Ensure config directory exists
|
||||
mkdir $config_dir
|
||||
|
||||
# STEP 4: Generate config from template
|
||||
let template_path = if ($group_path | is-not-empty) {
|
||||
$template_base_path | path join $group_path $"($module).k"
|
||||
} else {
|
||||
$template_base_path | path join $"($module).k"
|
||||
}
|
||||
|
||||
# Build import statement with "as {module}" alias
|
||||
let import_stmt = if ($group_path | is-not-empty) {
|
||||
$"import ($extension_type).($group_path).($module).ncl.($module) as ($module)"
|
||||
} else {
|
||||
$"import ($extension_type).($module).ncl.($module) as ($module)"
|
||||
}
|
||||
|
||||
# Get relative paths for comments
|
||||
let workspace_name = ($workspace_root | path basename)
|
||||
let relative_schema_path = if ($group_path | is-not-empty) {
|
||||
$"($workspace_name)/.($extension_type)/($group_path)/($module)"
|
||||
} else {
|
||||
$"($workspace_name)/.($extension_type)/($module)"
|
||||
}
|
||||
|
||||
let config_content = if ($template_path | path exists) {
|
||||
print $" 📄 Using template from: ($template_path)"
|
||||
let template_body = (open $template_path)
|
||||
$"# Configuration for ($module)
|
||||
# Workspace: ($workspace_name)
|
||||
# Schemas from: ($relative_schema_path)
|
||||
($import_stmt)
|
||||
|
||||
($template_body)"
|
||||
} else {
|
||||
$"# Configuration for ($module)
|
||||
# Workspace: ($workspace_name)
|
||||
# Schemas from: ($relative_schema_path)
|
||||
($import_stmt)
|
||||
|
||||
# TODO: Configure your ($module) instance
|
||||
# See available schemas at: ($relative_schema_path)/nickel/
|
||||
"
|
||||
}
|
||||
|
||||
$config_content | save -f $config_file_path
|
||||
print $" ✓ Config created: ($config_file_path)"
|
||||
print $" 📝 Edit ($extension_type)/($module).k to configure settings"
|
||||
|
||||
# STEP 3: Update infra nickel.mod
|
||||
if ($workspace_abs | str contains "/infra/") {
|
||||
let nickel_mod_path = ($workspace_abs | path join "nickel.mod")
|
||||
if ($nickel_mod_path | path exists) {
|
||||
let nickel_mod_content = (open $nickel_mod_path)
|
||||
if not ($nickel_mod_content | str contains $"($extension_type) =") {
|
||||
print $" 🔧 Updating nickel.mod to include ($extension_type) dependency"
|
||||
let new_dependency = $"\n# Workspace-level ($extension_type) \(shared across infras\)\n($extension_type) = { path = \"../../.($extension_type)\" }\n"
|
||||
$"($nickel_mod_content)($new_dependency)" | save -f $nickel_mod_path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Unload taskserv from workspace
|
||||
def unload_taskserv_from_workspace [workspace: string, module: string, layer: string] {
|
||||
let target_path = match $layer {
|
||||
"workspace" => ($workspace | path join "taskservs" $"($module).k")
|
||||
"infra" => ($workspace | path join "overrides" $"($module).k")
|
||||
_ => ($workspace | path join "taskservs" $"($module).k")
|
||||
}
|
||||
|
||||
if ($target_path | path exists) {
|
||||
rm $target_path
|
||||
print $" ✓ Removed: ($target_path)"
|
||||
} else {
|
||||
print $" ❌ Not found: ($target_path)"
|
||||
}
|
||||
}
|
||||
|
||||
# List workspace taskservs
|
||||
def list_workspace_taskservs [workspace: string, layer: string, format: string] {
|
||||
let paths = match $layer {
|
||||
"workspace" => [($workspace | path join "taskservs")]
|
||||
"infra" => [($workspace | path join "overrides")]
|
||||
"all" => [($workspace | path join "taskservs"), ($workspace | path join "overrides")]
|
||||
_ => [($workspace | path join "taskservs")]
|
||||
}
|
||||
|
||||
mut all_taskservs = []
|
||||
|
||||
for path in $paths {
|
||||
if ($path | path exists) {
|
||||
let taskservs = ls $path
|
||||
| where type == file
|
||||
| where name =~ '\\.k$'
|
||||
| each { |file|
|
||||
{
|
||||
name: ($file.name | path basename | str replace ".k" "")
|
||||
layer: ($path | path basename)
|
||||
path: $file.name
|
||||
modified: $file.modified
|
||||
}
|
||||
}
|
||||
$all_taskservs = ($all_taskservs | append $taskservs)
|
||||
}
|
||||
}
|
||||
|
||||
format_output $all_taskservs $format
|
||||
}
|
||||
|
||||
# Format output based on requested format
|
||||
def format_output [data: any, format: string] {
|
||||
match $format {
|
||||
"json" => ($data | to json)
|
||||
"yaml" => ($data | to yaml)
|
||||
"names" => ($data | get name | str join "\n")
|
||||
"table" | _ => ($data | table)
|
||||
}
|
||||
}
|
||||
|
||||
# === HELP FUNCTIONS ===
|
||||
|
||||
def print_enhanced_help [] {
|
||||
print "Enhanced Module Loader CLI - Discovery and loading with template support"
|
||||
print ""
|
||||
print "Usage: module-loader <command> [options]"
|
||||
print ""
|
||||
print "CORE COMMANDS:"
|
||||
print " discover <type> [query] [--format <fmt>] [--category <cat>] - Discover available modules"
|
||||
print " sync <infra> [--manifest <file>] [--show-modules] - Sync Nickel dependencies for infrastructure"
|
||||
print " load <type> <workspace> <modules...> [--layer <layer>] - Load modules into workspace"
|
||||
print " list <type> <workspace> [--layer <layer>] - List loaded modules"
|
||||
print " unload <type> <workspace> <module> [--layer <layer>] - Unload module from workspace"
|
||||
print ""
|
||||
print "WORKSPACE COMMANDS:"
|
||||
print " init <workspace> [--modules <list>] [--template <name>] - Initialize workspace"
|
||||
print " validate <workspace> - Validate workspace integrity"
|
||||
print " info <workspace> - Show workspace information"
|
||||
print ""
|
||||
print "TEMPLATE COMMANDS:"
|
||||
print " template list [--type <type>] [--format <fmt>] - List available templates"
|
||||
print " template extract <source> <name> [--type <type>] - Extract template from infra"
|
||||
print " template apply <template> <target> [--dry-run] - Apply template to infra"
|
||||
print ""
|
||||
print "LAYER COMMANDS:"
|
||||
print " layer show <workspace> [--module <name>] - Show layer information"
|
||||
print " layer test <module> <workspace> [provider] - Test layer resolution"
|
||||
print ""
|
||||
print "OVERRIDE COMMANDS:"
|
||||
print " override create <type> <infra> <module> [--from <template>] - Create configuration override"
|
||||
print ""
|
||||
print "ENHANCED COMMANDS:"
|
||||
print " load enhanced <type> <workspace> <infra> <modules> [--layer <layer>] - Enhanced template loading"
|
||||
print ""
|
||||
print "Types: taskservs, providers, clusters"
|
||||
print "Layers: workspace, infra, all"
|
||||
print "Formats: table, json, yaml, names"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader discover taskservs --category databases"
|
||||
print " module-loader load taskservs ./workspace [redis, postgres]"
|
||||
print " module-loader template list --type taskservs"
|
||||
print " module-loader layer test redis wuji upcloud"
|
||||
print " module-loader override create taskservs wuji kubernetes --from ha-cluster"
|
||||
}
|
||||
|
||||
def print_discover_help [] {
|
||||
print "Discover available modules"
|
||||
print ""
|
||||
print "Usage: module-loader discover <type> [query] [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --format <fmt> Output format: table, json, yaml, names (default: table)"
|
||||
print " --category <cat> Filter by category (taskservs only)"
|
||||
print " --group <group> Filter by group (taskservs only)"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader discover taskservs"
|
||||
print " module-loader discover taskservs redis"
|
||||
print " module-loader discover taskservs --category databases"
|
||||
print " module-loader discover taskservs --format json"
|
||||
}
|
||||
|
||||
def print_load_help [] {
|
||||
print "Load modules into workspace"
|
||||
print ""
|
||||
print "Usage: module-loader load <type> <workspace> <modules...> [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --layer <layer> Target layer: workspace, infra (default: workspace)"
|
||||
print " --validate Validate workspace after loading"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader load taskservs ./workspace [kubernetes, cilium]"
|
||||
print " module-loader load taskservs ./workspace [redis] --layer infra"
|
||||
}
|
||||
|
||||
def print_list_help [] {
|
||||
print "List modules in workspace"
|
||||
print ""
|
||||
print "Usage: module-loader list <type> <workspace> [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --layer <layer> Layer to list: workspace, infra, all (default: all)"
|
||||
print " --format <fmt> Output format: table, json, yaml, names"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader list taskservs ./workspace"
|
||||
print " module-loader list taskservs ./workspace --layer workspace"
|
||||
}
|
||||
|
||||
def print_unload_help [] {
|
||||
print "Unload module from workspace"
|
||||
print ""
|
||||
print "Usage: module-loader unload <type> <workspace> <module> [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --layer <layer> Layer to unload from: workspace, infra (default: workspace)"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader unload taskservs ./workspace kubernetes"
|
||||
print " module-loader unload taskservs ./workspace redis --layer infra"
|
||||
}
|
||||
|
||||
def print_init_help [] {
|
||||
print "Initialize workspace with modules"
|
||||
print ""
|
||||
print "Usage: module-loader init <workspace> [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --modules <list> Initial modules to load"
|
||||
print " --template <name> Workspace template to use"
|
||||
print " --provider <name> Default provider (default: upcloud)"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader init ./my-workspace"
|
||||
print " module-loader init ./k8s-workspace --modules [kubernetes, cilium]"
|
||||
}
|
||||
|
||||
def print_validate_help [] {
|
||||
print "Validate workspace integrity"
|
||||
print ""
|
||||
print "Usage: module-loader validate <workspace>"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader validate ./workspace"
|
||||
}
|
||||
|
||||
def print_info_help [] {
|
||||
print "Show workspace information"
|
||||
print ""
|
||||
print "Usage: module-loader info <workspace>"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader info ./workspace"
|
||||
}
|
||||
|
||||
def print_template_help [] {
|
||||
print "Template management commands"
|
||||
print ""
|
||||
print "Usage: module-loader template <subcommand> [options]"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " list List available templates"
|
||||
print " extract Extract template from existing infrastructure"
|
||||
print " apply Apply template to infrastructure"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader template list --type taskservs"
|
||||
print " module-loader template extract ./wuji wuji-production"
|
||||
print " module-loader template apply wuji-production ./new-infra"
|
||||
}
|
||||
|
||||
def print_layer_help [] {
|
||||
print "Layer resolution commands"
|
||||
print ""
|
||||
print "Usage: module-loader layer <subcommand> [options]"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " show Show layer information for workspace"
|
||||
print " test Test layer resolution for specific module"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader layer show ./workspace"
|
||||
print " module-loader layer test kubernetes wuji upcloud"
|
||||
}
|
||||
|
||||
def print_override_help [] {
|
||||
print "Configuration override commands"
|
||||
print ""
|
||||
print "Usage: module-loader override create <type> <infra> <module> [options]"
|
||||
print ""
|
||||
print "Options:"
|
||||
print " --from <template> Base override on template"
|
||||
print " --layer <layer> Target layer: infra, workspace (default: infra)"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " module-loader override create taskservs wuji kubernetes"
|
||||
print " module-loader override create taskservs wuji redis --from databases/redis"
|
||||
}
|
||||
752
cli/new_provisioning
Executable file
752
cli/new_provisioning
Executable file
|
|
@ -0,0 +1,752 @@
|
|||
#!/usr/bin/env bash
|
||||
# Info: Script to run Provisioning
|
||||
# Author: Jesus Perez Lorenzo
|
||||
# Release: 3.0.11
|
||||
# Date: 2026-01-14
|
||||
|
||||
set +o errexit
|
||||
set +o pipefail
|
||||
|
||||
# Debug: log startup
|
||||
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] Wrapper started with args: $@" >&2
|
||||
|
||||
export NU=$(type -P nu)
|
||||
|
||||
_release() {
|
||||
grep "^# Release:" "$0" | sed "s/# Release: //g"
|
||||
}
|
||||
|
||||
export PROVISIONING_VERS=$(_release)
|
||||
|
||||
set -o allexport
|
||||
## shellcheck disable=SC1090
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
[ -r "../env-provisioning" ] && source ../env-provisioning
|
||||
[ -r "env-provisioning" ] && source ./env-provisioning
|
||||
#[ -r ".env" ] && source .env set
|
||||
|
||||
# Disable provisioning logo/banner output
|
||||
export PROVISIONING_NO_TITLES=true
|
||||
|
||||
set +o allexport
|
||||
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.}
|
||||
|
||||
RUNNER="provisioning"
|
||||
PROVISIONING_MODULE=""
|
||||
PROVISIONING_MODULE_TASK=""
|
||||
|
||||
# Safe argument handling - use default empty value if unbound
|
||||
[ "${1:-}" == "" ] && shift
|
||||
|
||||
[ -z "$NU" ] || [ "${1:-}" == "install" ] || [ "${1:-}" == "reinstall" ] || [ "${1:-}" == "mode" ] && exec bash $PROVISIONING/core/bin/install_nu.sh $PROVISIONING ${1:-} ${2:-}
|
||||
|
||||
[ "${1:-}" == "rmwk" ] && rm -rf "$PROVIISONING_WKPATH"* && echo "$PROVIISONING_WKPATH deleted" && exit
|
||||
[ "${1:-}" == "-x" ] && debug=-x && export PROVISIONING_DEBUG=true && shift
|
||||
[ "${1:-}" == "-xm" ] && export PROVISIONING_METADATA=true && shift
|
||||
[ "${1:-}" == "nu" ] && export PROVISIONING_DEBUG=true
|
||||
[ "${1:-}" == "--x" ] && set -x && debug=-x && export PROVISIONING_DEBUG=true && shift
|
||||
[ "${1:-}" == "-i" ] || [ "${2:-}" == "-i" ] && echo "$(basename "$0") $(grep "^# Info:" "$0" | sed "s/# Info: //g") " && exit
|
||||
[ "${1:-}" == "-v" ] || [ "${1:-}" == "--version" ] || [ "${2:-}" == "-v" ] && _release && exit
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# FLOW-AWARE TTY COMMAND FILTER
|
||||
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
|
||||
# Registry: provisioning/core/cli/tty-commands.conf
|
||||
# Filter: provisioning/core/cli/tty-filter.sh
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
if [ -f "$PROVISIONING/core/cli/tty-filter.sh" ]; then
|
||||
# Source filter function
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROVISIONING/core/cli/tty-filter.sh"
|
||||
|
||||
# Try to filter TTY command (full command line as single string)
|
||||
# Return codes:
|
||||
# - filter_tty_command returns 0: flow=continue case handled, continue to Nushell with $TTY_OUTPUT
|
||||
# - filter_tty_command exits: flow=exit/pipe case completed (already exited)
|
||||
# - filter returns 1: not a TTY command, continue to normal processing
|
||||
if filter_tty_command "$@"; then
|
||||
# Flow=continue: TTY wrapper executed, output in $TTY_OUTPUT, bypass daemon
|
||||
# $env.PROVISIONING_BYPASS_DAEMON and $env.TTY_OUTPUT available to Nushell
|
||||
: # Continue to Nushell dispatcher below
|
||||
fi
|
||||
fi
|
||||
|
||||
CMD_ARGS=$@
|
||||
|
||||
# Note: Flag ordering is handled by Nushell's reorder_args function
|
||||
# which automatically reorders flags before positional arguments.
|
||||
# Flags can be placed anywhere on the command line.
|
||||
case "${1:-}" in
|
||||
# Note: "setup" is now handled by the main provisioning CLI dispatcher
|
||||
# No special module handling needed
|
||||
-mod)
|
||||
PROVISIONING_MODULE=$(echo "$2" | sed 's/ //g' | cut -f1 -d"|")
|
||||
PROVISIONING_MODULE_TASK=$(echo "$2" | sed 's/ //g' | cut -f2 -d"|")
|
||||
[ "$PROVISIONING_MODULE" == "$PROVISIONING_MODULE_TASK" ] && PROVISIONING_MODULE_TASK=""
|
||||
shift 2
|
||||
CMD_ARGS=$@
|
||||
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] -mod detected: MODULE=$PROVISIONING_MODULE, TASK=$PROVISIONING_MODULE_TASK, CMD_ARGS=$CMD_ARGS" >&2
|
||||
;;
|
||||
esac
|
||||
NU_ARGS=""
|
||||
|
||||
DEFAULT_CONTEXT_TEMPLATE="default_context.yaml"
|
||||
case "$(uname | tr '[:upper:]' '[:lower:]')" in
|
||||
linux)
|
||||
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
|
||||
;;
|
||||
darwin)
|
||||
PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/Library/Application Support/provisioning/platform"
|
||||
;;
|
||||
*)
|
||||
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive)
|
||||
# Falls back to traditional handlers if daemon unavailable
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DAEMON_ENDPOINT="http://127.0.0.1:9091/execute"
|
||||
|
||||
# Function to execute command via daemon
|
||||
execute_via_daemon() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
# Build JSON array of arguments (simple bash)
|
||||
local args_json="["
|
||||
local first=1
|
||||
for arg in "$@"; do
|
||||
[ $first -eq 0 ] && args_json="$args_json,"
|
||||
args_json="$args_json\"$(echo "$arg" | sed 's/"/\\"/g')\""
|
||||
first=0
|
||||
done
|
||||
args_json="$args_json]"
|
||||
|
||||
# Determine timeout based on command type
|
||||
# Heavy commands (create, delete, update) get longer timeout
|
||||
local timeout=0.5
|
||||
case "$cmd" in
|
||||
create | delete | update | setup | init) timeout=5 ;;
|
||||
*) timeout=0.2 ;;
|
||||
esac
|
||||
|
||||
# Make request and extract stdout
|
||||
curl -s -m $timeout -X POST "$DAEMON_ENDPOINT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\":\"$cmd\",\"args\":$args_json,\"timeout_ms\":30000}" 2>/dev/null |
|
||||
sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' |
|
||||
sed 's/\\n/\n/g'
|
||||
}
|
||||
|
||||
# Try daemon ONLY for lightweight commands (list, show, status)
|
||||
# Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow
|
||||
# ALSO skip daemon for flow=continue commands (need stdin for TTY interaction)
|
||||
if [ "${PROVISIONING_BYPASS_DAEMON:-}" != "true" ] && ([ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]); then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
# Light command - try daemon
|
||||
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚡ Attempting daemon execution..." >&2
|
||||
DAEMON_OUTPUT=$(execute_via_daemon "$@" 2>/dev/null)
|
||||
if [ -n "$DAEMON_OUTPUT" ]; then
|
||||
echo "$DAEMON_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚠️ Daemon unavailable, using traditional handlers..." >&2
|
||||
fi
|
||||
# NOTE: Command reordering (server create -> create server) has been removed.
|
||||
# The Nushell dispatcher in provisioning/core/nulib/main_provisioning/dispatcher.nu
|
||||
# handles command routing correctly and expects "server create" format.
|
||||
# The reorder_args function in provisioning script handles any flag reordering needed.
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# FAST-PATH: Commands that don't need full config loading or platform bootstrap
|
||||
# These commands use lib_minimal.nu for <100ms execution
|
||||
# (ONLY REACHED if daemon is not available)
|
||||
# ═══<E29590><E29590><EFBFBD>════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Help commands fast-path (uses help_minimal.nu)
|
||||
# Detects "help" in ANY argument position, not just first
|
||||
help_category=""
|
||||
help_found=false
|
||||
|
||||
# Check if first arg is empty (no args provided) - treat as help request
|
||||
if [ -z "${1:-}" ]; then
|
||||
help_found=true
|
||||
else
|
||||
# Loop through all arguments to find help variant and extract category
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
help|-h|--help|--helpinfo)
|
||||
help_found=true
|
||||
;;
|
||||
-*)
|
||||
# Skip flags (like -x, -xm, -i, -v, etc.)
|
||||
;;
|
||||
*)
|
||||
# First non-flag, non-help argument becomes the category
|
||||
if [ "$help_category" = "" ]; then
|
||||
help_category="$arg"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
# Execute help fast-path if help was requested
|
||||
if [ "$help_found" = true ]; then
|
||||
# Export LANG explicitly to ensure locale detection works in nu subprocess
|
||||
export LANG
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$help_category' | print" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Workspace operations (fast-path)
|
||||
if [ "${1:-}" = "workspace" ] || [ "${1:-}" = "ws" ]; then
|
||||
case "${2:-}" in
|
||||
"list" | "")
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-list | get ok | table" 2>/dev/null
|
||||
exit $?
|
||||
;;
|
||||
"active")
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active" 2>/dev/null
|
||||
exit $?
|
||||
;;
|
||||
"info")
|
||||
if [ -n "${3:-}" ]; then
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-info '$3'" 2>/dev/null
|
||||
else
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active | workspace-info \$in" 2>/dev/null
|
||||
fi
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
# Other workspace commands (switch, register, etc.) fall through to full loading
|
||||
fi
|
||||
|
||||
# Status/Health check (fast-path) - DISABLED to fix dispatcher loop
|
||||
# Use normal dispatcher path instead of fast-path with lib_minimal.nu
|
||||
# if [ "$1" = "status" ] || [ "$1" = "health" ]; then
|
||||
# $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null
|
||||
# exit $?
|
||||
# fi
|
||||
|
||||
# Environment display (fast-path)
|
||||
if [ "${1:-}" = "env" ] || [ "${1:-}" = "allenv" ]; then
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; env-quick | table" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Provider list (lightweight - reads filesystem only, no module loading)
|
||||
if [ "${1:-}" = "provider" ] || [ "${1:-}" = "providers" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
|
||||
let providers_base = (\$provisioning | path join 'extensions' | path join 'providers')
|
||||
|
||||
if not (\$providers_base | path exists) {
|
||||
print 'PROVIDERS list: (none found)'
|
||||
return
|
||||
}
|
||||
|
||||
# Discover all providers from directories
|
||||
let all_providers = (
|
||||
ls \$providers_base | where type == 'dir' | each {|prov_dir|
|
||||
let prov_name = (\$prov_dir.name | path basename)
|
||||
if \$prov_name != 'prov_lib' {
|
||||
{name: \$prov_name, type: 'providers', version: '0.0.1'}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} | compact
|
||||
)
|
||||
|
||||
if (\$all_providers | length) == 0 {
|
||||
print 'PROVIDERS list: (none found)'
|
||||
} else {
|
||||
print 'PROVIDERS list: '
|
||||
print ''
|
||||
\$all_providers | table
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Taskserv list (fast-path) - avoid full system load
|
||||
if [ "${1:-}" = "taskserv" ] || [ "${1:-}" = "task" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
# Direct implementation of taskserv discovery (no dependency loading)
|
||||
# Taskservs are nested: extensions/taskservs/{category}/{name}/kcl/
|
||||
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
|
||||
let taskservs_base = (\$provisioning | path join 'extensions' | path join 'taskservs')
|
||||
|
||||
if not (\$taskservs_base | path exists) {
|
||||
print '📦 Available Taskservs: (none found)'
|
||||
return null
|
||||
}
|
||||
|
||||
# Discover all taskservs from nested categories
|
||||
let all_taskservs = (
|
||||
ls \$taskservs_base | where type == 'dir' | each {|cat_dir|
|
||||
let category = (\$cat_dir.name | path basename)
|
||||
let cat_path = (\$taskservs_base | path join \$category)
|
||||
if (\$cat_path | path exists) {
|
||||
ls \$cat_path | where type == 'dir' | each {|ts|
|
||||
let ts_name = (\$ts.name | path basename)
|
||||
{task: \$ts_name, mode: \$category, info: ''}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
} | flatten
|
||||
)
|
||||
|
||||
if (\$all_taskservs | length) == 0 {
|
||||
print '📦 Available Taskservs: (none found)'
|
||||
} else {
|
||||
print '📦 Available Taskservs:'
|
||||
print ''
|
||||
\$all_taskservs | each {|ts|
|
||||
print \$\" • (\$ts.task) [(\$ts.mode)]\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Server list (lightweight - reads filesystem only, no config loading)
|
||||
if [ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
# Extract --infra flag from remaining args
|
||||
INFRA_FILTER=""
|
||||
shift
|
||||
[ "${1:-}" = "list" ] && shift
|
||||
while [ $# -gt 0 ]; do
|
||||
case "${1:-}" in
|
||||
--infra | -i)
|
||||
INFRA_FILTER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = if (\$env.HOME | path exists) {
|
||||
(
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
} else {
|
||||
''
|
||||
}
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
|
||||
if not (\$infra_path | path exists) {
|
||||
print 'No infrastructures found'
|
||||
return
|
||||
}
|
||||
|
||||
# Filter by infrastructure if specified
|
||||
let infra_filter = \"$INFRA_FILTER\"
|
||||
|
||||
# List server definitions from infrastructure (filtered if --infra specified)
|
||||
let servers = (
|
||||
ls \$infra_path | where type == 'dir' | each {|infra|
|
||||
let infra_name = (\$infra.name | path basename)
|
||||
|
||||
# Skip if filter is specified and doesn't match
|
||||
if ((\$infra_filter | is-not-empty) and (\$infra_name != \$infra_filter)) {
|
||||
[]
|
||||
} else {
|
||||
let servers_file = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.ncl')
|
||||
let servers_file_kcl = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.k')
|
||||
|
||||
if ($servers_file | path exists) {
|
||||
# Parse the Nickel servers.ncl file to extract server hostnames
|
||||
let content = (open \$servers_file --raw)
|
||||
# Extract hostnames from hostname = "..." patterns by splitting on quotes
|
||||
let hostnames = (
|
||||
\$content
|
||||
| split row \"\\n\"
|
||||
| where {|line| \$line | str contains \"hostname = \\\"\" }
|
||||
| each {|line|
|
||||
# Split by quotes to extract hostname value
|
||||
let parts = (\$line | split row \"\\\"\")
|
||||
if (\$parts | length) >= 2 {
|
||||
\$parts | get 1
|
||||
} else {
|
||||
\"\"
|
||||
}
|
||||
}
|
||||
| where {|h| (\$h | is-not-empty) }
|
||||
)
|
||||
|
||||
\$hostnames | each {|srv_name|
|
||||
{
|
||||
name: \$srv_name
|
||||
infrastructure: \$infra_name
|
||||
path: \$servers_file
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
} | flatten
|
||||
)
|
||||
|
||||
if (\$servers | length) == 0 {
|
||||
print '📦 Available Servers: (none configured)'
|
||||
} else {
|
||||
print '📦 Available Servers:'
|
||||
print ''
|
||||
\$servers | each {|srv|
|
||||
print \$\" • (\$srv.name) [(\$srv.infrastructure)]\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cluster list (lightweight - reads filesystem only)
|
||||
if [ "${1:-}" = "cluster" ] || [ "${1:-}" = "cl" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
|
||||
# List all clusters from workspace
|
||||
let clusters = (
|
||||
if ((\$ws_path | path join '.clusters') | path exists) {
|
||||
let clusters_path = (\$ws_path | path join '.clusters')
|
||||
ls \$clusters_path | where type == 'dir' | each {|cl|
|
||||
let cl_name = (\$cl.name | path basename)
|
||||
{
|
||||
name: \$cl_name
|
||||
path: \$cl.name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
if (\$clusters | length) == 0 {
|
||||
print '🗂️ Available Clusters: (none found)'
|
||||
} else {
|
||||
print '🗂️ Available Clusters:'
|
||||
print ''
|
||||
\$clusters | each {|cl|
|
||||
print \$\" • (\$cl.name)\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Infra list (lightweight - reads filesystem only)
|
||||
if [ "${1:-}" = "infra" ] || [ "${1:-}" = "inf" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
|
||||
if not (\$infra_path | path exists) {
|
||||
print '📁 Available Infrastructures: (none configured)'
|
||||
return
|
||||
}
|
||||
|
||||
# List all infrastructures
|
||||
let infras = (
|
||||
ls \$infra_path | where type == 'dir' | each {|inf|
|
||||
let inf_name = (\$inf.name | path basename)
|
||||
let inf_full_path = (\$infra_path | path join \$inf_name)
|
||||
let has_config = ((\$inf_full_path | path join 'settings.k') | path exists)
|
||||
|
||||
{
|
||||
name: \$inf_name
|
||||
configured: \$has_config
|
||||
modified: \$inf.modified
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (\$infras | length) == 0 {
|
||||
print '📁 Available Infrastructures: (none found)'
|
||||
} else {
|
||||
print '📁 Available Infrastructures:'
|
||||
print ''
|
||||
\$infras | each {|inf|
|
||||
let status = if \$inf.configured { '✓' } else { '○' }
|
||||
let output = \" [\" + \$status + \"] \" + \$inf.name
|
||||
print \$output
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Config validation (lightweight - validates config structure without full load)
|
||||
if [ "${1:-}" = "validate" ]; then
|
||||
if [ "${2:-}" = "config" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
try {
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print '❌ Error: No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print '❌ Error: User config not found at' \$user_config_path
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print '❌ Error: Workspace' \$active_ws 'not found in config'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
|
||||
# Validate workspace structure
|
||||
let required_dirs = ['infra', 'config', '.clusters']
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
let config_path = (\$ws_path | path join 'config')
|
||||
|
||||
let missing_dirs = \$required_dirs | where { not ((\$ws_path | path join \$in) | path exists) }
|
||||
|
||||
if (\$missing_dirs | length) > 0 {
|
||||
print '⚠️ Warning: Missing directories:' (\$missing_dirs | str join ', ')
|
||||
}
|
||||
|
||||
# Validate infrastructures have required files
|
||||
if (\$infra_path | path exists) {
|
||||
let infras = (ls \$infra_path | where type == 'dir')
|
||||
let invalid_infras = (
|
||||
\$infras | each {|inf|
|
||||
let inf_name = (\$inf.name | path basename)
|
||||
let inf_full_path = (\$infra_path | path join \$inf_name)
|
||||
if not ((\$inf_full_path | path join 'settings.k') | path exists) {
|
||||
\$inf_name
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} | compact
|
||||
)
|
||||
|
||||
if (\$invalid_infras | length) > 0 {
|
||||
print '⚠️ Warning: Infrastructures missing settings.k:' (\$invalid_infras | str join ', ')
|
||||
}
|
||||
}
|
||||
|
||||
# Validate user config structure
|
||||
let has_active = ((\$config | get --optional active_workspace) != null)
|
||||
let has_workspaces = ((\$config | get --optional workspaces) != null)
|
||||
let has_preferences = ((\$config | get --optional preferences) != null)
|
||||
|
||||
if not \$has_active {
|
||||
print '⚠️ Warning: Missing active_workspace in user config'
|
||||
}
|
||||
|
||||
if not \$has_workspaces {
|
||||
print '⚠️ Warning: Missing workspaces list in user config'
|
||||
}
|
||||
|
||||
if not \$has_preferences {
|
||||
print '⚠️ Warning: Missing preferences in user config'
|
||||
}
|
||||
|
||||
# Summary
|
||||
print ''
|
||||
print '✓ Configuration validation complete for workspace:' \$active_ws
|
||||
print ' Path:' \$ws_path
|
||||
print ' Status: Valid (with warnings, if any listed above)'
|
||||
} catch {|err|
|
||||
print '❌ Validation error:' \$err
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROVISIONING_USER_CONFIG" ] || [ ! -r "$PROVISIONING_CONTEXT_PATH" ]; then
|
||||
[ ! -x "$PROVISIONING/core/nulib/provisioning setup" ] && echo "$PROVISIONING/core/nulib/provisioning setup not found" && exit 1
|
||||
cd "$PROVISIONING/core/nulib"
|
||||
./"provisioning setup"
|
||||
echo ""
|
||||
read -p "Use [enter] to continue or [ctrl-c] to cancel"
|
||||
fi
|
||||
[ ! -r "$PROVISIONING_USER_CONFIG/config.nu" ] && echo "$PROVISIONING_USER_CONFIG/config.nu not found" && exit 1
|
||||
[ ! -r "$PROVISIONING_USER_CONFIG/env.nu" ] && echo "$PROVISIONING_USER_CONFIG/env.nu not found" && exit 1
|
||||
|
||||
NU_ARGS=(--config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu")
|
||||
export PROVISIONING_ARGS="$CMD_ARGS" NU_ARGS="$NU_ARGS"
|
||||
#export NU_ARGS=${NU_ARGS//Application Support/Application\\ Support}
|
||||
|
||||
# Suppress repetitive config export output during initialization
|
||||
export PROVISIONING_QUIET_EXPORT="true"
|
||||
|
||||
# Export NU_LIB_DIRS so Nushell can find modules during parsing
|
||||
export NU_LIB_DIRS="$PROVISIONING/core/nulib:/opt/provisioning/core/nulib:/usr/local/provisioning/core/nulib"
|
||||
|
||||
# ============================================================================
|
||||
# DAEMON ROUTING - ENABLED (Phase 3.7: CLI Daemon Integration)
|
||||
# ============================================================================
|
||||
# Redesigned daemon with pre-loaded Nushell environment (no CLI callback).
|
||||
# Routes eligible commands to HTTP daemon for <100ms execution.
|
||||
# Gracefully falls back to full load if daemon unavailable.
|
||||
#
|
||||
# ARCHITECTURE:
|
||||
# 1. Check daemon health (curl with 5ms timeout)
|
||||
# 2. Route eligible commands to daemon via HTTP POST
|
||||
# 3. Fall back to full load if daemon unavailable
|
||||
# 4. Zero breaking changes (graceful degradation)
|
||||
#
|
||||
# PERFORMANCE:
|
||||
# - With daemon: <100ms for ALL commands
|
||||
# - Without daemon: ~430ms (normal behavior)
|
||||
# - Daemon fallback: Automatic, user sees no difference
|
||||
|
||||
if [ -n "$PROVISIONING_MODULE" ]; then
|
||||
# When module is set, just run provisioning - it handles module routing internally
|
||||
export PROVISIONING_MODULE
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
|
||||
else
|
||||
# Only redirect stdin for non-interactive commands (nu command needs interactive stdin)
|
||||
if [ "${1:-}" = "nu" ]; then
|
||||
# For interactive mode, start nu with provisioning environment
|
||||
export PROVISIONING_CONFIG="$PROVISIONING_USER_CONFIG"
|
||||
# Start nu interactively - it will use the config and env from NU_ARGS
|
||||
$NU "${NU_ARGS[@]}"
|
||||
else
|
||||
# Don't redirect stdin for infrastructure commands - they may need interactive input
|
||||
# Only redirect for commands we know are safe
|
||||
case "${1:-}" in
|
||||
help | h | --help | --info | -i | -v | --version | env | allenv | status | health | list | ls | l | workspace | ws | provider | providers | validate | plugin | plugins | nuinfo | platform | plat)
|
||||
# Safe commands - can use /dev/null
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS </dev/null
|
||||
;;
|
||||
*)
|
||||
# All other commands (create, delete, server, taskserv, etc.) - keep stdin open
|
||||
# NOTE: PROVISIONING_MODULE is automatically inherited by Nushell from bash environment
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
754
cli/old_provisioning
Normal file
754
cli/old_provisioning
Normal file
|
|
@ -0,0 +1,754 @@
|
|||
#!/usr/bin/env bash
|
||||
# Info: Script to run Provisioning
|
||||
# Author: Jesus Perez Lorenzo
|
||||
# Release: 3.0.11
|
||||
# Date: 2026-01-14
|
||||
|
||||
set +o errexit
|
||||
set +o pipefail
|
||||
|
||||
# Debug: log startup
|
||||
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] Wrapper started with args: $@" >&2
|
||||
|
||||
export NU=$(type -P nu)
|
||||
|
||||
_release() {
|
||||
grep "^# Release:" "$0" | sed "s/# Release: //g"
|
||||
}
|
||||
|
||||
export PROVISIONING_VERS=$(_release)
|
||||
|
||||
set -o allexport
|
||||
## shellcheck disable=SC1090
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
[ -r "../env-provisioning" ] && source ../env-provisioning
|
||||
[ -r "env-provisioning" ] && source ./env-provisioning
|
||||
#[ -r ".env" ] && source .env set
|
||||
|
||||
# Disable provisioning logo/banner output
|
||||
export PROVISIONING_NO_TITLES=true
|
||||
|
||||
set +o allexport
|
||||
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.}
|
||||
|
||||
RUNNER="provisioning"
|
||||
PROVISIONING_MODULE=""
|
||||
PROVISIONING_MODULE_TASK=""
|
||||
|
||||
# Safe argument handling - use default empty value if unbound
|
||||
[ "${1:-}" == "" ] && shift
|
||||
|
||||
[ -z "$NU" ] || [ "${1:-}" == "install" ] || [ "${1:-}" == "reinstall" ] || [ "${1:-}" == "mode" ] && exec bash $PROVISIONING/core/bin/install_nu.sh $PROVISIONING ${1:-} ${2:-}
|
||||
|
||||
[ "${1:-}" == "rmwk" ] && rm -rf "$PROVIISONING_WKPATH"* && echo "$PROVIISONING_WKPATH deleted" && exit
|
||||
[ "${1:-}" == "-x" ] && debug=-x && export PROVISIONING_DEBUG=true && shift
|
||||
[ "${1:-}" == "-xm" ] && export PROVISIONING_METADATA=true && shift
|
||||
[ "${1:-}" == "nu" ] && export PROVISIONING_DEBUG=true
|
||||
[ "${1:-}" == "--x" ] && set -x && debug=-x && export PROVISIONING_DEBUG=true && shift
|
||||
[ "${1:-}" == "-i" ] || [ "${2:-}" == "-i" ] && echo "$(basename "$0") $(grep "^# Info:" "$0" | sed "s/# Info: //g") " && exit
|
||||
[ "${1:-}" == "-v" ] || [ "${1:-}" == "--version" ] || [ "${2:-}" == "-v" ] && _release && exit
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# FLOW-AWARE TTY COMMAND FILTER
|
||||
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
|
||||
# Registry: provisioning/core/cli/tty-commands.conf
|
||||
# Filter: provisioning/core/cli/tty-filter.sh
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
if [ -f "$PROVISIONING/core/cli/tty-filter.sh" ]; then
|
||||
# Source filter function
|
||||
# shellcheck source=/dev/null
|
||||
source "$PROVISIONING/core/cli/tty-filter.sh"
|
||||
|
||||
# Try to filter TTY command (full command line as single string)
|
||||
# Return codes:
|
||||
# - filter_tty_command returns 0: flow=continue case handled, continue to Nushell with $TTY_OUTPUT
|
||||
# - filter_tty_command exits: flow=exit/pipe case completed (already exited)
|
||||
# - filter returns 1: not a TTY command, continue to normal processing
|
||||
if filter_tty_command "$@"; then
|
||||
# Flow=continue: TTY wrapper executed, output in $TTY_OUTPUT, bypass daemon
|
||||
# $env.PROVISIONING_BYPASS_DAEMON and $env.TTY_OUTPUT available to Nushell
|
||||
: # Continue to Nushell dispatcher below
|
||||
fi
|
||||
fi
|
||||
|
||||
CMD_ARGS=$@
|
||||
|
||||
# Note: Flag ordering is handled by Nushell's reorder_args function
|
||||
# which automatically reorders flags before positional arguments.
|
||||
# Flags can be placed anywhere on the command line.
|
||||
case "${1:-}" in
|
||||
# Note: "setup" is now handled by the main provisioning CLI dispatcher
|
||||
# No special module handling needed
|
||||
-mod)
|
||||
PROVISIONING_MODULE=$(echo "$2" | sed 's/ //g' | cut -f1 -d"|")
|
||||
PROVISIONING_MODULE_TASK=$(echo "$2" | sed 's/ //g' | cut -f2 -d"|")
|
||||
[ "$PROVISIONING_MODULE" == "$PROVISIONING_MODULE_TASK" ] && PROVISIONING_MODULE_TASK=""
|
||||
shift 2
|
||||
CMD_ARGS=$@
|
||||
[ "${PROVISIONING_DEBUG_STARTUP:-false}" = "true" ] && echo "[DEBUG] -mod detected: MODULE=$PROVISIONING_MODULE, TASK=$PROVISIONING_MODULE_TASK, CMD_ARGS=$CMD_ARGS" >&2
|
||||
;;
|
||||
esac
|
||||
NU_ARGS=""
|
||||
|
||||
DEFAULT_CONTEXT_TEMPLATE="default_context.yaml"
|
||||
case "$(uname | tr '[:upper:]' '[:lower:]')" in
|
||||
linux)
|
||||
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
|
||||
;;
|
||||
darwin)
|
||||
PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/Library/Application Support/provisioning/platform"
|
||||
;;
|
||||
*)
|
||||
PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell"
|
||||
PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE"
|
||||
PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive)
|
||||
# Falls back to traditional handlers if daemon unavailable
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
DAEMON_ENDPOINT="http://127.0.0.1:9091/execute"
|
||||
|
||||
# Function to execute command via daemon
|
||||
execute_via_daemon() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
# Build JSON array of arguments (simple bash)
|
||||
local args_json="["
|
||||
local first=1
|
||||
for arg in "$@"; do
|
||||
[ $first -eq 0 ] && args_json="$args_json,"
|
||||
args_json="$args_json\"$(echo "$arg" | sed 's/"/\\"/g')\""
|
||||
first=0
|
||||
done
|
||||
args_json="$args_json]"
|
||||
|
||||
# Determine timeout based on command type
|
||||
# Heavy commands (create, delete, update) get longer timeout
|
||||
local timeout=0.5
|
||||
case "$cmd" in
|
||||
create | delete | update | setup | init) timeout=5 ;;
|
||||
*) timeout=0.2 ;;
|
||||
esac
|
||||
|
||||
# Make request and extract stdout
|
||||
curl -s -m $timeout -X POST "$DAEMON_ENDPOINT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\":\"$cmd\",\"args\":$args_json,\"timeout_ms\":30000}" 2>/dev/null |
|
||||
sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' |
|
||||
sed 's/\\n/\n/g'
|
||||
}
|
||||
|
||||
# Try daemon ONLY for lightweight commands (list, show, status)
|
||||
# Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow
|
||||
# ALSO skip daemon for flow=continue commands (need stdin for TTY interaction)
|
||||
if [ "${PROVISIONING_BYPASS_DAEMON:-}" != "true" ] && ([ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]); then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
# Light command - try daemon
|
||||
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚡ Attempting daemon execution..." >&2
|
||||
DAEMON_OUTPUT=$(execute_via_daemon "$@" 2>/dev/null)
|
||||
if [ -n "$DAEMON_OUTPUT" ]; then
|
||||
echo "$DAEMON_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
[ -n "${PROVISIONING_DEBUG:-}" ] && [ "${PROVISIONING_DEBUG:-}" = "true" ] && echo "⚠️ Daemon unavailable, using traditional handlers..." >&2
|
||||
fi
|
||||
# NOTE: Command reordering (server create -> create server) has been removed.
|
||||
# The Nushell dispatcher in provisioning/core/nulib/main_provisioning/dispatcher.nu
|
||||
# handles command routing correctly and expects "server create" format.
|
||||
# The reorder_args function in provisioning script handles any flag reordering needed.
|
||||
fi
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════════════════
|
||||
# FAST-PATH: Commands that don't need full config loading or platform bootstrap
|
||||
# These commands use lib_minimal.nu for <100ms execution
|
||||
# (ONLY REACHED if daemon is not available)
|
||||
# ═══<E29590><E29590><EFBFBD>════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Help commands fast-path (uses help_minimal.nu)
|
||||
# Detects "help" in ANY argument position, not just first
|
||||
help_category=""
|
||||
help_found=false
|
||||
|
||||
# Check if first arg is empty (no args provided) - treat as help request
|
||||
if [ -z "${1:-}" ]; then
|
||||
help_found=true
|
||||
else
|
||||
# Loop through all arguments to find help variant and extract category
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
help|-h|--help|--helpinfo)
|
||||
help_found=true
|
||||
;;
|
||||
-*)
|
||||
# Skip flags (like -x, -xm, -i, -v, etc.)
|
||||
;;
|
||||
*)
|
||||
# First non-flag, non-help argument becomes the category
|
||||
if [ "$help_category" = "" ]; then
|
||||
help_category="$arg"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
# Execute help fast-path if help was requested
|
||||
if [ "$help_found" = true ]; then
|
||||
# Export LANG explicitly to ensure locale detection works in nu subprocess
|
||||
export LANG
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$help_category' | print" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Workspace operations (fast-path)
|
||||
if [ "${1:-}" = "workspace" ] || [ "${1:-}" = "ws" ]; then
|
||||
case "${2:-}" in
|
||||
"list" | "")
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-list | get ok | table" 2>/dev/null
|
||||
exit $?
|
||||
;;
|
||||
"active")
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active" 2>/dev/null
|
||||
exit $?
|
||||
;;
|
||||
"info")
|
||||
if [ -n "${3:-}" ]; then
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-info '$3'" 2>/dev/null
|
||||
else
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active | workspace-info \$in" 2>/dev/null
|
||||
fi
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
# Other workspace commands (switch, register, etc.) fall through to full loading
|
||||
fi
|
||||
|
||||
# Status/Health check (fast-path) - DISABLED to fix dispatcher loop
|
||||
# Use normal dispatcher path instead of fast-path with lib_minimal.nu
|
||||
# if [ "$1" = "status" ] || [ "$1" = "health" ]; then
|
||||
# $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null
|
||||
# exit $?
|
||||
# fi
|
||||
|
||||
# Environment display (fast-path)
|
||||
if [ "${1:-}" = "env" ] || [ "${1:-}" = "allenv" ]; then
|
||||
$NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; env-quick | table" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Provider list (lightweight - reads filesystem only, no module loading)
|
||||
if [ "${1:-}" = "provider" ] || [ "${1:-}" = "providers" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
|
||||
let providers_base = (\$provisioning | path join 'extensions' | path join 'providers')
|
||||
|
||||
if not (\$providers_base | path exists) {
|
||||
print 'PROVIDERS list: (none found)'
|
||||
return
|
||||
}
|
||||
|
||||
# Discover all providers from directories
|
||||
let all_providers = (
|
||||
ls \$providers_base | where type == 'dir' | each {|prov_dir|
|
||||
let prov_name = (\$prov_dir.name | path basename)
|
||||
if \$prov_name != 'prov_lib' {
|
||||
{name: \$prov_name, type: 'providers', version: '0.0.1'}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} | compact
|
||||
)
|
||||
|
||||
if (\$all_providers | length) == 0 {
|
||||
print 'PROVIDERS list: (none found)'
|
||||
} else {
|
||||
print 'PROVIDERS list: '
|
||||
print ''
|
||||
\$all_providers | table
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Taskserv list (fast-path) - avoid full system load
|
||||
if [ "${1:-}" = "taskserv" ] || [ "${1:-}" = "task" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
# Direct implementation of taskserv discovery (no dependency loading)
|
||||
# Taskservs are nested: extensions/taskservs/{category}/{name}/kcl/
|
||||
let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning')
|
||||
let taskservs_base = (\$provisioning | path join 'extensions' | path join 'taskservs')
|
||||
|
||||
if not (\$taskservs_base | path exists) {
|
||||
print '📦 Available Taskservs: (none found)'
|
||||
return null
|
||||
}
|
||||
|
||||
# Discover all taskservs from nested categories
|
||||
let all_taskservs = (
|
||||
ls \$taskservs_base | where type == 'dir' | each {|cat_dir|
|
||||
let category = (\$cat_dir.name | path basename)
|
||||
let cat_path = (\$taskservs_base | path join \$category)
|
||||
if (\$cat_path | path exists) {
|
||||
ls \$cat_path | where type == 'dir' | each {|ts|
|
||||
let ts_name = (\$ts.name | path basename)
|
||||
{task: \$ts_name, mode: \$category, info: ''}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
} | flatten
|
||||
)
|
||||
|
||||
if (\$all_taskservs | length) == 0 {
|
||||
print '📦 Available Taskservs: (none found)'
|
||||
} else {
|
||||
print '📦 Available Taskservs:'
|
||||
print ''
|
||||
\$all_taskservs | each {|ts|
|
||||
print \$\" • (\$ts.task) [(\$ts.mode)]\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Server list (lightweight - reads filesystem only, no config loading)
|
||||
if [ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
# Extract --infra flag from remaining args
|
||||
INFRA_FILTER=""
|
||||
shift
|
||||
[ "${1:-}" = "list" ] && shift
|
||||
while [ $# -gt 0 ]; do
|
||||
case "${1:-}" in
|
||||
--infra | -i)
|
||||
INFRA_FILTER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = if (\$env.HOME | path exists) {
|
||||
(
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
} else {
|
||||
''
|
||||
}
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
|
||||
if not (\$infra_path | path exists) {
|
||||
print 'No infrastructures found'
|
||||
return
|
||||
}
|
||||
|
||||
# Filter by infrastructure if specified
|
||||
let infra_filter = \"$INFRA_FILTER\"
|
||||
|
||||
# List server definitions from infrastructure (filtered if --infra specified)
|
||||
let servers = (
|
||||
ls \$infra_path | where type == 'dir' | each {|infra|
|
||||
let infra_name = (\$infra.name | path basename)
|
||||
|
||||
# Skip if filter is specified and doesn't match
|
||||
if ((\$infra_filter | is-not-empty) and (\$infra_name != \$infra_filter)) {
|
||||
[]
|
||||
} else {
|
||||
let servers_file = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.ncl')
|
||||
let servers_file_kcl = ($infra_path | path join \$infra_name | path join 'defs' | path join 'servers.k')
|
||||
|
||||
if ($servers_file | path exists) {
|
||||
# Parse the Nickel servers.ncl file to extract server hostnames
|
||||
let content = (open \$servers_file --raw)
|
||||
# Extract hostnames from hostname = "..." patterns by splitting on quotes
|
||||
let hostnames = (
|
||||
\$content
|
||||
| split row \"\\n\"
|
||||
| where {|line| \$line | str contains \"hostname = \\\"\" }
|
||||
| each {|line|
|
||||
# Split by quotes to extract hostname value
|
||||
let parts = (\$line | split row \"\\\"\")
|
||||
if (\$parts | length) >= 2 {
|
||||
\$parts | get 1
|
||||
} else {
|
||||
\"\"
|
||||
}
|
||||
}
|
||||
| where {|h| (\$h | is-not-empty) }
|
||||
)
|
||||
|
||||
\$hostnames | each {|srv_name|
|
||||
{
|
||||
name: \$srv_name
|
||||
infrastructure: \$infra_name
|
||||
path: \$servers_file
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
} | flatten
|
||||
)
|
||||
|
||||
if (\$servers | length) == 0 {
|
||||
print '📦 Available Servers: (none configured)'
|
||||
} else {
|
||||
print '📦 Available Servers:'
|
||||
print ''
|
||||
\$servers | each {|srv|
|
||||
print \$\" • (\$srv.name) [(\$srv.infrastructure)]\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cluster list (lightweight - reads filesystem only)
|
||||
if [ "${1:-}" = "cluster" ] || [ "${1:-}" = "cl" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
|
||||
# List all clusters from workspace
|
||||
let clusters = (
|
||||
if ((\$ws_path | path join '.clusters') | path exists) {
|
||||
let clusters_path = (\$ws_path | path join '.clusters')
|
||||
ls \$clusters_path | where type == 'dir' | each {|cl|
|
||||
let cl_name = (\$cl.name | path basename)
|
||||
{
|
||||
name: \$cl_name
|
||||
path: \$cl.name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
if (\$clusters | length) == 0 {
|
||||
print '🗂️ Available Clusters: (none found)'
|
||||
} else {
|
||||
print '🗂️ Available Clusters:'
|
||||
print ''
|
||||
\$clusters | each {|cl|
|
||||
print \$\" • (\$cl.name)\"
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Infra list (lightweight - reads filesystem only)
|
||||
if [ "${1:-}" = "infra" ] || [ "${1:-}" = "inf" ]; then
|
||||
if [ "${2:-}" = "list" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print 'No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print 'Config not found'
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print 'Workspace not found'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
|
||||
if not (\$infra_path | path exists) {
|
||||
print '📁 Available Infrastructures: (none configured)'
|
||||
return
|
||||
}
|
||||
|
||||
# List all infrastructures
|
||||
let infras = (
|
||||
ls \$infra_path | where type == 'dir' | each {|inf|
|
||||
let inf_name = (\$inf.name | path basename)
|
||||
let inf_full_path = (\$infra_path | path join \$inf_name)
|
||||
let has_config = ((\$inf_full_path | path join 'settings.k') | path exists)
|
||||
|
||||
{
|
||||
name: \$inf_name
|
||||
configured: \$has_config
|
||||
modified: \$inf.modified
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (\$infras | length) == 0 {
|
||||
print '📁 Available Infrastructures: (none found)'
|
||||
} else {
|
||||
print '📁 Available Infrastructures:'
|
||||
print ''
|
||||
\$infras | each {|inf|
|
||||
let status = if \$inf.configured { '✓' } else { '○' }
|
||||
let output = \" [\" + \$status + \"] \" + \$inf.name
|
||||
print \$output
|
||||
} | ignore
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
# Config validation (lightweight - validates config structure without full load)
|
||||
if [ "${1:-}" = "validate" ]; then
|
||||
if [ "${2:-}" = "config" ] || [ -z "${2:-}" ]; then
|
||||
$NU -n -c "
|
||||
source '$PROVISIONING/core/nulib/lib_minimal.nu'
|
||||
|
||||
try {
|
||||
# Get active workspace
|
||||
let active_ws = (workspace-active)
|
||||
if (\$active_ws | is-empty) {
|
||||
print '❌ Error: No active workspace'
|
||||
return
|
||||
}
|
||||
|
||||
# Get workspace path from config
|
||||
let user_config_path = (
|
||||
\$env.HOME | path join 'Library' | path join 'Application Support' |
|
||||
path join 'provisioning' | path join 'user_config.yaml'
|
||||
)
|
||||
|
||||
if not (\$user_config_path | path exists) {
|
||||
print '❌ Error: User config not found at' \$user_config_path
|
||||
return
|
||||
}
|
||||
|
||||
let config = (open \$user_config_path)
|
||||
let workspaces = (\$config | get --optional workspaces | default [])
|
||||
let ws = (\$workspaces | where { \$in.name == \$active_ws } | first)
|
||||
|
||||
if (\$ws | is-empty) {
|
||||
print '❌ Error: Workspace' \$active_ws 'not found in config'
|
||||
return
|
||||
}
|
||||
|
||||
let ws_path = \$ws.path
|
||||
|
||||
# Validate workspace structure
|
||||
let required_dirs = ['infra', 'config', '.clusters']
|
||||
let infra_path = (\$ws_path | path join 'infra')
|
||||
let config_path = (\$ws_path | path join 'config')
|
||||
|
||||
let missing_dirs = \$required_dirs | where { not ((\$ws_path | path join \$in) | path exists) }
|
||||
|
||||
if (\$missing_dirs | length) > 0 {
|
||||
print '⚠️ Warning: Missing directories:' (\$missing_dirs | str join ', ')
|
||||
}
|
||||
|
||||
# Validate infrastructures have required files
|
||||
if (\$infra_path | path exists) {
|
||||
let infras = (ls \$infra_path | where type == 'dir')
|
||||
let invalid_infras = (
|
||||
\$infras | each {|inf|
|
||||
let inf_name = (\$inf.name | path basename)
|
||||
let inf_full_path = (\$infra_path | path join \$inf_name)
|
||||
if not ((\$inf_full_path | path join 'settings.k') | path exists) {
|
||||
\$inf_name
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} | compact
|
||||
)
|
||||
|
||||
if (\$invalid_infras | length) > 0 {
|
||||
print '⚠️ Warning: Infrastructures missing settings.k:' (\$invalid_infras | str join ', ')
|
||||
}
|
||||
}
|
||||
|
||||
# Validate user config structure
|
||||
let has_active = ((\$config | get --optional active_workspace) != null)
|
||||
let has_workspaces = ((\$config | get --optional workspaces) != null)
|
||||
let has_preferences = ((\$config | get --optional preferences) != null)
|
||||
|
||||
if not \$has_active {
|
||||
print '⚠️ Warning: Missing active_workspace in user config'
|
||||
}
|
||||
|
||||
if not \$has_workspaces {
|
||||
print '⚠️ Warning: Missing workspaces list in user config'
|
||||
}
|
||||
|
||||
if not \$has_preferences {
|
||||
print '⚠️ Warning: Missing preferences in user config'
|
||||
}
|
||||
|
||||
# Summary
|
||||
print ''
|
||||
print '✓ Configuration validation complete for workspace:' \$active_ws
|
||||
print ' Path:' \$ws_path
|
||||
print ' Status: Valid (with warnings, if any listed above)'
|
||||
} catch {|err|
|
||||
print '❌ Validation error:' \$err
|
||||
}
|
||||
" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$PROVISIONING_USER_CONFIG" ] || [ ! -r "$PROVISIONING_CONTEXT_PATH" ]; then
|
||||
[ ! -x "$PROVISIONING/core/nulib/provisioning setup" ] && echo "$PROVISIONING/core/nulib/provisioning setup not found" && exit 1
|
||||
cd "$PROVISIONING/core/nulib"
|
||||
./"provisioning setup"
|
||||
echo ""
|
||||
read -p "Use [enter] to continue or [ctrl-c] to cancel"
|
||||
fi
|
||||
[ ! -r "$PROVISIONING_USER_CONFIG/config.nu" ] && echo "$PROVISIONING_USER_CONFIG/config.nu not found" && exit 1
|
||||
[ ! -r "$PROVISIONING_USER_CONFIG/env.nu" ] && echo "$PROVISIONING_USER_CONFIG/env.nu not found" && exit 1
|
||||
|
||||
NU_ARGS=(--config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu")
|
||||
export PROVISIONING_ARGS="$CMD_ARGS" NU_ARGS="$NU_ARGS"
|
||||
#export NU_ARGS=${NU_ARGS//Application Support/Application\\ Support}
|
||||
|
||||
# Suppress repetitive config export output during initialization
|
||||
export PROVISIONING_QUIET_EXPORT="true"
|
||||
|
||||
# Export NU_LIB_DIRS so Nushell can find modules during parsing
|
||||
export NU_LIB_DIRS="$PROVISIONING/core/nulib:/opt/provisioning/core/nulib:/usr/local/provisioning/core/nulib"
|
||||
|
||||
# ============================================================================
|
||||
# DAEMON ROUTING - ENABLED (Phase 3.7: CLI Daemon Integration)
|
||||
# ============================================================================
|
||||
# Redesigned daemon with pre-loaded Nushell environment (no CLI callback).
|
||||
# Routes eligible commands to HTTP daemon for <100ms execution.
|
||||
# Gracefully falls back to full load if daemon unavailable.
|
||||
#
|
||||
# ARCHITECTURE:
|
||||
# 1. Check daemon health (curl with 5ms timeout)
|
||||
# 2. Route eligible commands to daemon via HTTP POST
|
||||
# 3. Fall back to full load if daemon unavailable
|
||||
# 4. Zero breaking changes (graceful degradation)
|
||||
#
|
||||
# PERFORMANCE:
|
||||
# - With daemon: <100ms for ALL commands
|
||||
# - Without daemon: ~430ms (normal behavior)
|
||||
# - Daemon fallback: Automatic, user sees no difference
|
||||
|
||||
if [ -n "$PROVISIONING_MODULE" ]; then
|
||||
if [[ -x $PROVISIONING/core/nulib/$RUNNER\ $PROVISIONING_MODULE ]]; then
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE" $CMD_ARGS
|
||||
else
|
||||
echo "Error \"$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE\" not found"
|
||||
fi
|
||||
else
|
||||
# Only redirect stdin for non-interactive commands (nu command needs interactive stdin)
|
||||
if [ "${1:-}" = "nu" ]; then
|
||||
# For interactive mode, start nu with provisioning environment
|
||||
export PROVISIONING_CONFIG="$PROVISIONING_USER_CONFIG"
|
||||
# Start nu interactively - it will use the config and env from NU_ARGS
|
||||
$NU "${NU_ARGS[@]}"
|
||||
else
|
||||
# Don't redirect stdin for infrastructure commands - they may need interactive input
|
||||
# Only redirect for commands we know are safe
|
||||
case "${1:-}" in
|
||||
help | h | --help | --info | -i | -v | --version | env | allenv | status | health | list | ls | l | workspace | ws | provider | providers | validate | plugin | plugins | nuinfo | platform | plat)
|
||||
# Safe commands - can use /dev/null
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS </dev/null
|
||||
;;
|
||||
*)
|
||||
# All other commands (create, delete, server, taskserv, etc.) - keep stdin open
|
||||
# NOTE: PROVISIONING_MODULE is automatically inherited by Nushell from bash environment
|
||||
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
224
cli/pack
Executable file
224
cli/pack
Executable file
|
|
@ -0,0 +1,224 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# KCL Packaging CLI
|
||||
# Package and distribute KCL modules for provisioning system
|
||||
# Author: JesusPerezLorenzo
|
||||
# Date: 2025-09-29
|
||||
|
||||
use ../nulib/lib_provisioning/kcl_packaging.nu *
|
||||
|
||||
# Main pack command
|
||||
def main [] {
|
||||
print_help
|
||||
}
|
||||
|
||||
# Package core provisioning schemas
|
||||
export def "main core" [
|
||||
--output: string = "", # Output directory
|
||||
--version: string = "" # Version override
|
||||
] {
|
||||
print "📦 Packaging Core Provisioning Schemas"
|
||||
print ""
|
||||
|
||||
let package_path = (pack-core --output $output --version $version)
|
||||
|
||||
print ""
|
||||
print $"✅ Core package created: ($package_path)"
|
||||
}
|
||||
|
||||
# Package a specific provider
|
||||
export def "main provider" [
|
||||
provider: string, # Provider name (e.g., upcloud, aws, local)
|
||||
--output: string = "", # Output directory
|
||||
--version: string = "" # Version override
|
||||
] {
|
||||
print $"📦 Packaging Provider: ($provider)"
|
||||
print ""
|
||||
|
||||
let package_path = (pack-provider $provider --output $output --version $version)
|
||||
|
||||
print ""
|
||||
print $"✅ Provider package created: ($package_path)"
|
||||
}
|
||||
|
||||
# Package all providers
|
||||
export def "main providers" [
|
||||
--output: string = "" # Output directory
|
||||
] {
|
||||
print "📦 Packaging All Providers"
|
||||
print ""
|
||||
|
||||
let results = (pack-all-providers --output $output)
|
||||
|
||||
print ""
|
||||
print "📊 Packaging Summary:"
|
||||
$results | table
|
||||
|
||||
let success_count = ($results | where status == "success" | length)
|
||||
let total_count = ($results | length)
|
||||
|
||||
print ""
|
||||
if $success_count == $total_count {
|
||||
print $"✅ All ($total_count) providers packaged successfully"
|
||||
} else {
|
||||
print $"⚠️ ($success_count)/($total_count) providers packaged successfully"
|
||||
}
|
||||
}
|
||||
|
||||
# Package everything (core + all providers)
|
||||
export def "main all" [
|
||||
--output: string = "" # Output directory
|
||||
] {
|
||||
print "📦 Packaging Everything (Core + All Providers)"
|
||||
print ""
|
||||
|
||||
# Package core
|
||||
print "▶️ Packaging core..."
|
||||
main core --output $output
|
||||
|
||||
print ""
|
||||
|
||||
# Package all providers
|
||||
print "▶️ Packaging providers..."
|
||||
main providers --output $output
|
||||
|
||||
print ""
|
||||
print "✅ Complete packaging finished"
|
||||
}
|
||||
|
||||
# List packaged modules
|
||||
export def "main list" [
|
||||
--format: string = "table" # Output format: table, json, yaml
|
||||
] {
|
||||
print "📦 Packaged Modules:"
|
||||
print ""
|
||||
|
||||
list-packages --format $format
|
||||
}
|
||||
|
||||
# Clean old packages
|
||||
export def "main clean" [
|
||||
--keep-latest: int = 3, # Number of latest versions to keep
|
||||
--dry-run, # Show what would be deleted
|
||||
--all, # Clean ALL packages (ignores keep-latest)
|
||||
--force # Skip confirmation prompts
|
||||
] {
|
||||
if $all {
|
||||
clean-all-packages --dry-run=$dry_run --force=$force
|
||||
} else {
|
||||
clean-packages --keep-latest $keep_latest --dry-run=$dry_run
|
||||
}
|
||||
}
|
||||
|
||||
# Remove specific package and its metadata
|
||||
export def "main remove" [
|
||||
package_name: string, # Package name (e.g., "aws_prov", "upcloud_prov", "provisioning_core")
|
||||
--force # Skip confirmation prompt
|
||||
] {
|
||||
remove-package $package_name --force=$force
|
||||
}
|
||||
|
||||
# Show package information
|
||||
export def "main info" [
|
||||
package_name: string # Package name (without .tar.gz)
|
||||
] {
|
||||
print $"📋 Package Information: ($package_name)"
|
||||
print ""
|
||||
|
||||
# Look for package
|
||||
let pack_path = (get-config-value "distribution" "pack_path")
|
||||
let package_file = ($pack_path | path join $"($package_name).tar.gz")
|
||||
|
||||
if not ($package_file | path exists) {
|
||||
print $"❌ Package not found: ($package_file)"
|
||||
return
|
||||
}
|
||||
|
||||
let info = (ls $package_file | first)
|
||||
|
||||
print $" File: ($info.name)"
|
||||
print $" Size: ($info.size)"
|
||||
print $" Modified: ($info.modified)"
|
||||
|
||||
# Check for metadata
|
||||
let registry_path = (get-config-value "distribution" "registry_path")
|
||||
let metadata_file = ($registry_path | path join $"($package_name).json")
|
||||
|
||||
if ($metadata_file | path exists) {
|
||||
print ""
|
||||
print " Metadata:"
|
||||
let metadata = (open $metadata_file)
|
||||
|
||||
print $" Name: ($metadata.name)"
|
||||
print $" Version: ($metadata.version)"
|
||||
print $" Created: ($metadata.created)"
|
||||
print $" Maintainer: ($metadata.maintainer)"
|
||||
print $" License: ($metadata.license)"
|
||||
print $" Repository: ($metadata.repository)"
|
||||
}
|
||||
}
|
||||
|
||||
# Initialize distribution directories
|
||||
export def "main init" [] {
|
||||
print "🚀 Initializing Distribution System"
|
||||
print ""
|
||||
|
||||
let pack_path = (get-config-value "distribution" "pack_path")
|
||||
let registry_path = (get-config-value "distribution" "registry_path")
|
||||
let cache_path = (get-config-value "distribution" "cache_path")
|
||||
|
||||
mkdir $pack_path
|
||||
print $" ✓ Created: ($pack_path)"
|
||||
|
||||
mkdir $registry_path
|
||||
print $" ✓ Created: ($registry_path)"
|
||||
|
||||
mkdir $cache_path
|
||||
print $" ✓ Created: ($cache_path)"
|
||||
|
||||
print ""
|
||||
print "✅ Distribution system initialized"
|
||||
}
|
||||
|
||||
# Helper: Print help
|
||||
def print_help [] {
|
||||
print "KCL Packaging CLI - Package and distribute KCL modules"
|
||||
print ""
|
||||
print "Usage: pack <command> [options]"
|
||||
print ""
|
||||
print "COMMANDS:"
|
||||
print " init - Initialize distribution directories"
|
||||
print " core [--output <dir>] [--version <v>] - Package core provisioning schemas"
|
||||
print " provider <name> [--output <dir>] - Package specific provider"
|
||||
print " providers [--output <dir>] - Package all providers"
|
||||
print " all [--output <dir>] - Package everything (core + providers)"
|
||||
print " list [--format <fmt>] - List packaged modules"
|
||||
print " info <package_name> - Show package information"
|
||||
print " remove <package_name> [--force] - Remove specific package and metadata"
|
||||
print " clean [--keep-latest <n>] [--all] [--dry-run] - Clean old packages"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --output <dir> Output directory (uses config default if not specified)"
|
||||
print " --version <v> Version override for package"
|
||||
print " --format <fmt> Output format: table, json, yaml"
|
||||
print " --keep-latest <n> Number of latest versions to keep (default: 3)"
|
||||
print " --all Clean ALL packages (use with clean command)"
|
||||
print " --force Skip confirmation prompts"
|
||||
print " --dry-run Show what would be done without executing"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " pack init"
|
||||
print " pack core"
|
||||
print " pack provider upcloud"
|
||||
print " pack providers"
|
||||
print " pack all"
|
||||
print " pack list"
|
||||
print " pack info provisioning_core"
|
||||
print " pack remove aws_prov"
|
||||
print " pack remove upcloud_prov --force"
|
||||
print " pack clean --keep-latest 5 --dry-run"
|
||||
print " pack clean --all --dry-run"
|
||||
print " pack clean --all"
|
||||
print ""
|
||||
print "Distribution configuration in: provisioning/config/config.defaults.toml [distribution]"
|
||||
}
|
||||
172
cli/port-manager
Executable file
172
cli/port-manager
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# Port Manager - Centralized port configuration management
|
||||
# Manages all platform service ports from a single source of truth
|
||||
|
||||
const PORTS_CONFIG = "/Users/Akasha/project-provisioning/provisioning/config/ports.toml"
|
||||
|
||||
# Show current port status
|
||||
export def main [] {
|
||||
print "🔧 Provisioning Platform Port Manager\n"
|
||||
print "Usage:"
|
||||
print " port-manager check - Check which ports are in use"
|
||||
print " port-manager list - List all configured ports"
|
||||
print " port-manager update - Update ports from config file"
|
||||
print " port-manager conflicts - Show port conflicts"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Check port availability
|
||||
export def "main check" [] {
|
||||
print "🔍 Checking port availability\n"
|
||||
|
||||
let ports = load_ports_config
|
||||
|
||||
for port in ($ports | transpose service config) {
|
||||
let port_num = $port.config.port
|
||||
let service_name = $port.service
|
||||
|
||||
try {
|
||||
let check = (^lsof -i $":($port_num)" | complete)
|
||||
|
||||
if $check.exit_code == 0 {
|
||||
let lines = ($check.stdout | lines)
|
||||
if ($lines | length) > 1 {
|
||||
let process_line = ($lines | get 1 | split row -r '\s+')
|
||||
let process_name = ($process_line | get 0)
|
||||
let pid = ($process_line | get 1)
|
||||
|
||||
print $" ⚠️ ($service_name | fill -a r -w 20): Port ($port_num) IN USE by ($process_name) \(PID: ($pid)\)"
|
||||
} else {
|
||||
print $" ✅ ($service_name | fill -a r -w 20): Port ($port_num) available"
|
||||
}
|
||||
} else {
|
||||
print $" ✅ ($service_name | fill -a r -w 20): Port ($port_num) available"
|
||||
}
|
||||
} catch {
|
||||
print $" ✅ ($service_name | fill -a r -w 20): Port ($port_num) available"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List all configured ports
|
||||
export def "main list" [] {
|
||||
print "📋 Configured Ports\n"
|
||||
|
||||
let ports = load_ports_config
|
||||
|
||||
print $"╭────────────────────────┬──────┬──────────────────────────────╮"
|
||||
print $"│ Service │ Port │ Description │"
|
||||
print $"├────────────────────────┼──────┼──────────────────────────────┤"
|
||||
|
||||
for port in ($ports | transpose service config) {
|
||||
let service = ($port.service | fill -a r -w 22)
|
||||
let port_num = ($port.config.port | into string | fill -a r -w 4)
|
||||
let desc = ($port.config.description | str substring 0..28 | fill -a r -w 28)
|
||||
|
||||
print $"│ ($service) │ ($port_num) │ ($desc) │"
|
||||
}
|
||||
|
||||
print $"╰────────────────────────┴──────┴──────────────────────────────╯"
|
||||
}
|
||||
|
||||
# Show port conflicts
|
||||
export def "main conflicts" [] {
|
||||
print "⚠️ Checking for port conflicts\n"
|
||||
|
||||
let ports = load_ports_config
|
||||
let conflicts = []
|
||||
|
||||
for port in ($ports | transpose service config) {
|
||||
let port_num = $port.config.port
|
||||
|
||||
try {
|
||||
let check = (^lsof -i $":($port_num)" | complete)
|
||||
|
||||
if $check.exit_code == 0 {
|
||||
let lines = ($check.stdout | lines)
|
||||
if ($lines | length) > 1 {
|
||||
let process_line = ($lines | get 1 | split row -r '\s+')
|
||||
let process_name = ($process_line | get 0)
|
||||
let pid = ($process_line | get 1)
|
||||
|
||||
print $" 🔴 ($port.service): Port ($port_num) is used by ($process_name) \(PID: ($pid)\)"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Port is free
|
||||
}
|
||||
}
|
||||
|
||||
print "\n💡 To free a port, stop the conflicting process or change the service port"
|
||||
}
|
||||
|
||||
# Update service ports from configuration
|
||||
export def "main update" [
|
||||
service?: string # Service to update (optional, updates all if not provided)
|
||||
--dry-run # Show what would be changed without applying
|
||||
] {
|
||||
let ports = load_ports_config
|
||||
|
||||
if $service != null {
|
||||
if $service not-in ($ports | columns) {
|
||||
error make {msg: $"Service '($service)' not found in configuration"}
|
||||
}
|
||||
|
||||
print $"📝 Updating ($service)..."
|
||||
update_service_port $service ($ports | get $service | get port) $dry_run
|
||||
} else {
|
||||
print "📝 Updating all services...\n"
|
||||
|
||||
for svc in ($ports | transpose service config) {
|
||||
update_service_port $svc.service $svc.config.port $dry_run
|
||||
}
|
||||
}
|
||||
|
||||
if $dry_run {
|
||||
print "\n💡 Run without --dry-run to apply changes"
|
||||
} else {
|
||||
print "\n✅ Update complete. Remember to rebuild and restart services!"
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: Load ports configuration
|
||||
def load_ports_config [] {
|
||||
if not ($PORTS_CONFIG | path exists) {
|
||||
# Return defaults if config doesn't exist
|
||||
return {
|
||||
orchestrator: {port: 9090, description: "Workflow orchestration engine"},
|
||||
control_center: {port: 9080, description: "Auth & authorization service"},
|
||||
api_gateway: {port: 9083, description: "Unified API gateway"},
|
||||
mcp_server: {port: 9082, description: "Model Context Protocol server"},
|
||||
oci_registry: {port: 5000, description: "OCI registry"},
|
||||
coredns: {port: 5353, description: "Internal DNS server"},
|
||||
gitea: {port: 3000, description: "Git server"},
|
||||
frontend: {port: 3001, description: "Web UI"},
|
||||
surrealdb: {port: 8000, description: "Main database"},
|
||||
redis: {port: 6379, description: "Cache"},
|
||||
postgresql: {port: 5432, description: "Optional database"}
|
||||
}
|
||||
}
|
||||
|
||||
open $PORTS_CONFIG
|
||||
}
|
||||
|
||||
# Helper: Update a single service port
|
||||
def update_service_port [service: string, port: int, dry_run: bool] {
|
||||
print $" Updating ($service) to port ($port)..."
|
||||
|
||||
# This would update config files, Rust code, etc.
|
||||
# For now, just show what would be done
|
||||
if $dry_run {
|
||||
print $" Would update configuration files for ($service)"
|
||||
print $" Would update Rust defaults"
|
||||
print $" Would update documentation"
|
||||
} else {
|
||||
print $" ⚠️ Automated updates not yet implemented"
|
||||
print $" Please update manually:"
|
||||
print $" 1. Update code defaults"
|
||||
print $" 2. Update config files"
|
||||
print $" 3. Rebuild service"
|
||||
}
|
||||
}
|
||||
278
cli/providers-install
Executable file
278
cli/providers-install
Executable file
|
|
@ -0,0 +1,278 @@
|
|||
#!/bin/bash
|
||||
# Info: Script to install providers
|
||||
# Author: JesusPerezLorenzo
|
||||
# Release: 1.0
|
||||
# Date: 12-11-2023
|
||||
|
||||
[ "$DEBUG" == "-x" ] && set -x
|
||||
|
||||
USAGE="install-tools [ tool-name: tera k9s, etc | all] [--update]
|
||||
As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces)
|
||||
Versions are set in ./versions file
|
||||
|
||||
This can be called by directly with an argumet or from an other srcipt
|
||||
"
|
||||
|
||||
ORG=$(pwd)
|
||||
|
||||
function _install_cmds {
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
local has_cmd
|
||||
for cmd in $CMDS_PROVISIONING
|
||||
do
|
||||
has_cmd=$(type -P $cmd)
|
||||
if [ -z "$has_cmd" ] ; then
|
||||
case "$(OS)" in
|
||||
darwin) brew install $cmd ;;
|
||||
linux) sudo apt install $cmd ;;
|
||||
*) echo "Install $cmd in your PATH" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function _install_tools {
|
||||
local match=$1
|
||||
shift
|
||||
local options
|
||||
options="$*"
|
||||
# local has_jq
|
||||
# local jq_version
|
||||
# local has_yq
|
||||
# local yq_version
|
||||
local has_nickel
|
||||
local nickel_version
|
||||
local has_tera
|
||||
local tera_version
|
||||
local has_k9s
|
||||
local k9s_version
|
||||
local has_age
|
||||
local age_version
|
||||
local has_sops
|
||||
local sops_version
|
||||
# local has_upctl
|
||||
# local upctl_version
|
||||
# local has_aws
|
||||
# local aws_version
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
ORG_OS=$(uname)
|
||||
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
|
||||
ORG_ARCH="$(uname -m)"
|
||||
|
||||
if [ -z "$CHECK_ONLY" ] and [ "$match" == "all" ] ; then
|
||||
_install_cmds
|
||||
fi
|
||||
|
||||
# if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then
|
||||
# has_jq=$(type -P jq)
|
||||
# num_version="0"
|
||||
# [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./}
|
||||
# expected_version_num=${JQ_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" &&
|
||||
# chmod +x "jq-${OS}-${ARCH}" &&
|
||||
# sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq &&
|
||||
# printf "%s\t%s\n" "jq" "installed $JQ_VERSION"
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "jq" "$jq_version" "expected $JQ_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "jq" "already $JQ_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
# if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then
|
||||
# has_yq=$(type -P yq)
|
||||
# num_version="0"
|
||||
# [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./}
|
||||
# expected_version_num=${YQ_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# tar -xzf "yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq &&
|
||||
# sudo ./install-man-page.sh &&
|
||||
# rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# printf "%s\t%s\n" "yq" "installed $YQ_VERSION"
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "yq" "$yq_version" "expected $YQ_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "yq" "already $YQ_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
if [ -n "$NICKEL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nickel" ] ; then
|
||||
has_nickel=$(type -P nickel)
|
||||
num_version="0"
|
||||
[ -n "$has_nickel" ] && nickel_version=$(nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) && num_version=${nickel_version//\./}
|
||||
expected_version_num=${NICKEL_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
echo "⚠️ Nickel installation/update required"
|
||||
echo " Recommended method: nix profile install nixpkgs#nickel"
|
||||
echo " Alternative: cargo install nickel-lang-cli --version ${NICKEL_VERSION}"
|
||||
echo " https://nickel-lang.org/getting-started"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "nickel" "$nickel_version" "expected $NICKEL_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "nickel" "already $NICKEL_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then
|
||||
has_tera=$(type -P tera)
|
||||
num_version="0"
|
||||
[ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./}
|
||||
expected_version_num=${TERA_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then
|
||||
sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION"
|
||||
else
|
||||
echo "Error: $(dirname "$0")/../ttools/tera_${OS}_${ARCH} not found !!"
|
||||
exit 2
|
||||
fi
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "tera" "already $TERA_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$K9S_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "k9s" ] ; then
|
||||
has_k9s=$(type -P k9s)
|
||||
num_version="0"
|
||||
[ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./}
|
||||
expected_version_num=${K9S_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
mkdir -p k9s && cd k9s &&
|
||||
curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz &&
|
||||
tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" &&
|
||||
sudo mv k9s /usr/local/bin &&
|
||||
cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" &&
|
||||
printf "%s\t%s\n" "k9s" "installed $K9S_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "k9s" "$k9s_version" "expected $K9S_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "k9s" "already $K9S_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$AGE_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "age" ] ; then
|
||||
has_age=$(type -P age)
|
||||
num_version="0"
|
||||
[ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./}
|
||||
expected_version_num=${AGE_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
|
||||
tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
|
||||
sudo mv age/age /usr/local/bin &&
|
||||
sudo mv age/age-keygen /usr/local/bin &&
|
||||
rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" &&
|
||||
printf "%s\t%s\n" "age" "installed $AGE_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "age" "already $AGE_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$SOPS_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "sops" ] ; then
|
||||
has_sops=$(type -P sops)
|
||||
num_version="0"
|
||||
[ -n "$has_sops" ] && sops_version="$(sops -v | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./}
|
||||
expected_version_num=${SOPS_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
mkdir -p sops && cd sops &&
|
||||
curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} &&
|
||||
mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
|
||||
chmod +x sops &&
|
||||
sudo mv sops /usr/local/bin &&
|
||||
rm -f sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
|
||||
printf "%s\t%s\n" "sops" "installed $SOPS_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "sops" "$sops_version" "expected $SOPS_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "sops" "already $SOPS_VERSION"
|
||||
fi
|
||||
fi
|
||||
# if [ -n "$UPCTL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "upctl" ] ; then
|
||||
# has_upctl=$(type -P upctl)
|
||||
# num_version="0"
|
||||
# [ -n "$has_upctl" ] && upctl_version=$(upctl version | grep "Version" | cut -f2 -d":" | sed 's/ //g') && num_version=${upctl_version//\./}
|
||||
# expected_version_num=${UPCTL_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# mkdir -p upctl && cd upctl &&
|
||||
# curl -fsSLO https://github.com/UpCloudLtd/upcloud-cli/releases/download/v${UPCTL_VERSION}/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz &&
|
||||
# tar -xzf "upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz" &&
|
||||
# sudo mv upctl /usr/local/bin &&
|
||||
# cd "$ORG" && rm -rf /tmp/upct "/upcloud-cli_${UPCTL_VERSION}_${OS}_${ORG_ARCH}.tar.gz"
|
||||
# printf "%s\t%s\n" "upctl" "installed $UPCTL_VERSION"
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "upctl" "$upctl_version" "expected $UPCTL_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "upctl" "already $UPCTL_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
# if [ -n "$AWS_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "aws" ] ; then
|
||||
# [ -r "/usr/bin/aws" ] && mv /usr/bin/aws /usr/bin/_aws
|
||||
# has_aws=$(type -P aws)
|
||||
# num_version="0"
|
||||
# [ -n "$has_aws" ] && aws_version=$(aws --version | cut -f1 -d" " | sed 's,aws-cli/,,g') && num_version=${aws_version//\./}
|
||||
# expected_version_num=${AWS_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# cd "$ORG" || exit 1
|
||||
# curl "https://awscli.amazonaws.com/awscli-exe-${OS}-${ORG_ARCH}.zip" -o "awscliv2.zip"
|
||||
# unzip awscliv2.zip >/dev/null
|
||||
# [ "$1" != "-update" ] && [ -d "/usr/local/aws-cli" ] && sudo rm -rf "/usr/local/aws-cli"
|
||||
# sudo ./aws/install && printf "%s\t%s\n" "aws" "installed $AWS_VERSION"
|
||||
# #sudo ./aws/install $options && echo "aws cli installed"
|
||||
# cd "$ORG" && rm -rf awscliv2.zip
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "aws" "$aws_version" "expected $AWS_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "aws" "already $AWS_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
}
|
||||
|
||||
function get_providers {
|
||||
local list
|
||||
local name
|
||||
|
||||
for item in $PROVIDERS_PATH/*
|
||||
do
|
||||
name=$(basename $item)
|
||||
[[ "$name" == _* ]] && continue
|
||||
[ ! -d "$item/templates" ] && [ ! -r "$item/provisioning.yam" ] && continue
|
||||
if [ -z "$list" ] ; then
|
||||
list="$name"
|
||||
else
|
||||
list="$list $name"
|
||||
fi
|
||||
done
|
||||
echo $list
|
||||
}
|
||||
function _on_providers {
|
||||
local providers_list=$1
|
||||
[ -z "$providers_list" ] || [[ "$providers_list" == -* ]] && providers_list=${PROVISIONING_PROVIDERS:-all}
|
||||
if [ "$providers_list" == "all" ] ; then
|
||||
providers_list=$(get_providers)
|
||||
fi
|
||||
for provider in $providers_list
|
||||
do
|
||||
[ ! -d "$PROVIDERS_PATH/$provider/templates" ] && [ ! -r "$PROVIDERS_PATH/$provider/provisioning.yam" ] && continue
|
||||
if [ ! -r "$PROVIDERS_PATH/$provider/bin/install.sh" ] ; then
|
||||
echo "🛑 Error on $provider no $PROVIDERS_PATH/$provider/bin/install.sh found"
|
||||
continue
|
||||
fi
|
||||
"$PROVIDERS_PATH/$provider/bin/install.sh" "$@"
|
||||
done
|
||||
}
|
||||
|
||||
set -o allexport
|
||||
## shellcheck disable=SC1090
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
[ -r "../env-provisioning" ] && source ../env-provisioning
|
||||
[ -r "env-provisioning" ] && source ./env-provisioning
|
||||
#[ -r ".env" ] && source .env set
|
||||
set +o allexport
|
||||
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
export PROVIDERS_PATH=${PROVIDERS_PATH:-"$PROVISIONING/providers"}
|
||||
|
||||
[ "$1" == "-h" ] && echo "$USAGE" && shift
|
||||
[ "$1" == "check" ] && CHECK_ONLY="yes" && shift
|
||||
[ -n "$1" ] && cd /tmp && _on_providers "$@"
|
||||
1318
cli/provisioning
Executable file
1318
cli/provisioning
Executable file
File diff suppressed because it is too large
Load diff
486
cli/tools-install
Executable file
486
cli/tools-install
Executable file
|
|
@ -0,0 +1,486 @@
|
|||
#!/bin/bash
|
||||
# Info: Script to install tools
|
||||
# Author: JesusPerezLorenzo
|
||||
# Release: 1.0
|
||||
# Date: 12-11-2023
|
||||
|
||||
[ "$DEBUG" == "-x" ] && set -x
|
||||
|
||||
USAGE="install-tools [ tool-name: providers tera k9s, etc | all] [--update]
|
||||
As alternative use environment var TOOL_TO_INSTALL with a list-of-tools (separeted with spaces)
|
||||
Versions are set in ./versions file
|
||||
|
||||
This can be called by directly with an argumet or from an other srcipt
|
||||
"
|
||||
|
||||
ORG=$(pwd)
|
||||
|
||||
function _install_cmds {
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
local has_cmd
|
||||
for cmd in $CMDS_PROVISIONING
|
||||
do
|
||||
has_cmd=$(type -P $cmd)
|
||||
if [ -z "$has_cmd" ] ; then
|
||||
case "$OS" in
|
||||
darwin) brew install $cmd ;;
|
||||
linux) sudo apt install $cmd ;;
|
||||
*) echo "Install $cmd in your PATH" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
}
|
||||
function _install_providers {
|
||||
local match=$1
|
||||
shift
|
||||
local options
|
||||
local info_keys
|
||||
options="$*"
|
||||
info_keys="info version site"
|
||||
if [ -z "$match" ] || [ "$match" == "all" ] || [ "$match" == "-" ]; then
|
||||
match="all"
|
||||
fi
|
||||
|
||||
for prov in $(ls $PROVIDERS_PATH | grep -v "^_" )
|
||||
do
|
||||
prov_name=$(basename "$prov")
|
||||
[ ! -d "$PROVIDERS_PATH/$prov_name/templates" ] && continue
|
||||
if [ "$match" == "all" ] || [ "$prov_name" == "$match" ] ; then
|
||||
[ -x "$PROVIDERS_PATH/$prov_name/bin/install.sh" ] && $PROVIDERS_PATH/$prov_name/bin/install.sh $options
|
||||
elif [ "$match" == "?" ] ; then
|
||||
[ -n "$options" ] && [ -z "$(echo "$options" | grep ^$prov_name)" ] && continue
|
||||
if [ -r "$PROVIDERS_PATH/$prov_name/provisioning.yaml" ] ; then
|
||||
echo "-------------------------------------------------------"
|
||||
for key in $info_keys
|
||||
do
|
||||
echo -n "$key:"
|
||||
[ "$key" != "version" ] && echo -ne "\t"
|
||||
echo " $(grep "^$key:" "$PROVIDERS_PATH/$prov_name/provisioning.yaml" | sed "s/$key: //g")"
|
||||
done
|
||||
[ -n "$options" ] && echo "________________________________________________________"
|
||||
else
|
||||
echo "$prov_name"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
[ "$match" == "?" ] && [ -z "$options" ] && echo "________________________________________________________"
|
||||
}
|
||||
function _install_tools {
|
||||
local match=$1
|
||||
shift
|
||||
local options
|
||||
options="$*"
|
||||
# local has_jq
|
||||
# local jq_version
|
||||
# local has_yq
|
||||
# local yq_version
|
||||
local has_nu
|
||||
local nu_version
|
||||
local has_nickel
|
||||
local nickel_version
|
||||
local has_tera
|
||||
local tera_version
|
||||
local has_k9s
|
||||
local k9s_version
|
||||
local has_age
|
||||
local age_version
|
||||
local has_sops
|
||||
local sops_version
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
ORG_OS=$(uname)
|
||||
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')"
|
||||
ORG_ARCH="$(uname -m)"
|
||||
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$match" == "all" ] ; then
|
||||
_install_cmds
|
||||
fi
|
||||
|
||||
# if [ -n "$JQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "jq" ] ; then
|
||||
# has_jq=$(type -P jq)
|
||||
# num_version="0"
|
||||
# [ -n "$has_jq" ] && jq_version=$(jq -V | sed 's/jq-//g') && num_version=${jq_version//\./}
|
||||
# expected_version_num=${JQ_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# curl -fsSLO "https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-${OS}-${ARCH}" &&
|
||||
# chmod +x "jq-${OS}-${ARCH}" &&
|
||||
# sudo mv "jq-${OS}-${ARCH}" /usr/local/bin/jq &&
|
||||
# printf "%s\t%s\n" "jq" "installed $JQ_VERSION"
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "jq" "$jq_version" "expected $JQ_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "jq" "already $JQ_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
# if [ -n "$YQ_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "yq" ] ; then
|
||||
# has_yq=$(type -P yq)
|
||||
# num_version="0"
|
||||
# [ -n "$has_yq" ] && yq_version=$(yq -V | cut -f4 -d" " | sed 's/v//g') && num_version=${yq_version//\./}
|
||||
# expected_version_num=${YQ_VERSION//\./}
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# curl -fsSLO "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# tar -xzf "yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# sudo mv "yq_${OS}_${ARCH}" /usr/local/bin/yq &&
|
||||
# sudo ./install-man-page.sh &&
|
||||
# rm -f install-man-page.sh yq.1 "yq_${OS}_${ARCH}.tar.gz" &&
|
||||
# printf "%s\t%s\n" "yq" "installed $YQ_VERSION"
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "yq" "$yq_version" "expected $YQ_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "yq" "already $YQ_VERSION"
|
||||
# fi
|
||||
# fi
|
||||
|
||||
if [ -n "$NU_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nu" ] ; then
|
||||
has_nu=$(type -P nu)
|
||||
num_version="0"
|
||||
[ -n "$has_nu" ] && nu_version=$(nu -v) && num_version=${nu_version//\./} && num_version=${num_version//0/}
|
||||
expected_version_num=${NU_VERSION//\./}
|
||||
expected_version_num=${expected_version_num//0/}
|
||||
[ -z "$num_version" ] && num_version=0
|
||||
if [ -z "$num_version" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation"
|
||||
elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION require installation"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "nu" "$nu_version" "expected $NU_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "nu" "already $NU_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$NICKEL_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "nickel" ] ; then
|
||||
has_nickel=$(type -P nickel)
|
||||
num_version=0
|
||||
[ -n "$has_nickel" ] && nickel_version=$((nickel -V | cut -f3 -d" ") 2>/dev/null) && num_version=${nickel_version//\./}
|
||||
expected_version_num=${NICKEL_VERSION//\./}
|
||||
[ -z "$num_version" ] && num_version=0
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# macOS: try Cargo first, then Homebrew
|
||||
if [ "$OS" == "darwin" ] ; then
|
||||
printf "%s\t%s\n" "nickel" "installing $NICKEL_VERSION on macOS"
|
||||
|
||||
# Try Cargo first (if available)
|
||||
if command -v cargo >/dev/null 2>&1 ; then
|
||||
printf "%s\t%s\n" "nickel" "using Cargo (Rust compiler)"
|
||||
if cargo install nickel-lang-cli --version "${NICKEL_VERSION}" ; then
|
||||
printf "%s\t%s\n" "nickel" "✅ installed $NICKEL_VERSION via Cargo"
|
||||
else
|
||||
printf "%s\t%s\n" "nickel" "❌ Failed to build with Cargo"
|
||||
exit 1
|
||||
fi
|
||||
# Try Homebrew if Cargo not available
|
||||
elif command -v brew >/dev/null 2>&1 ; then
|
||||
printf "%s\t%s\n" "nickel" "using Homebrew"
|
||||
if brew install nickel ; then
|
||||
printf "%s\t%s\n" "nickel" "✅ installed $NICKEL_VERSION via Homebrew"
|
||||
else
|
||||
printf "%s\t%s\n" "nickel" "❌ Failed to install with Homebrew"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Neither Cargo nor Homebrew available
|
||||
printf "%s\t%s\n" "nickel" "⚠️ Neither Cargo nor Homebrew found"
|
||||
printf "%s\t%s\n" "nickel" "Install one of:"
|
||||
printf "%s\t%s\n" "nickel" " 1. Cargo: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
||||
printf "%s\t%s\n" "nickel" " 2. Homebrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Non-macOS: download binary from GitHub
|
||||
printf "%s\t%s\n" "nickel" "installing $NICKEL_VERSION on $OS"
|
||||
|
||||
# Map architecture names (GitHub uses different naming)
|
||||
local nickel_arch="$ARCH"
|
||||
[ "$nickel_arch" == "amd64" ] && nickel_arch="x86_64"
|
||||
|
||||
# Build download URL
|
||||
local download_url="https://github.com/tweag/nickel/releases/download/${NICKEL_VERSION}/nickel-${nickel_arch}-${OS}"
|
||||
|
||||
# Download and install
|
||||
if curl -fsSLO "$download_url" && chmod +x "nickel-${nickel_arch}-${OS}" && sudo mv "nickel-${nickel_arch}-${OS}" /usr/local/bin/nickel ; then
|
||||
printf "%s\t%s\n" "nickel" "installed $NICKEL_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "nickel" "❌ Failed to download/install Nickel binary"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "nickel" "$nickel_version" "expected $NICKEL_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "nickel" "already $NICKEL_VERSION"
|
||||
fi
|
||||
fi
|
||||
#if [ -n "$TERA_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "tera" ] ; then
|
||||
# has_tera=$(type -P tera)
|
||||
# num_version="0"
|
||||
# [ -n "$has_tera" ] && tera_version=$(tera -V | cut -f2 -d" " | sed 's/teracli//g') && num_version=${tera_version//\./}
|
||||
# expected_version_num=${TERA_VERSION//\./}
|
||||
# [ -z "$num_version" ] && num_version=0
|
||||
# if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
# if [ -x "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" ] ; then
|
||||
# sudo cp "$(dirname "$0")/../tools/tera_${OS}_${ARCH}" /usr/local/bin/tera && printf "%s\t%s\n" "tera" "installed $TERA_VERSION"
|
||||
# else
|
||||
# echo "Error: $(dirname "$0")/../tools/tera_${OS}_${ARCH} not found !!"
|
||||
# exit 2
|
||||
# fi
|
||||
# elif [ -n "$CHECK_ONLY" ] ; then
|
||||
# printf "%s\t%s\t%s\n" "tera" "$tera_version" "expected $TERA_VERSION"
|
||||
# else
|
||||
# printf "%s\t%s\n" "tera" "already $TERA_VERSION"
|
||||
# fi
|
||||
#fi
|
||||
if [ -n "$K9S_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "k9s" ] ; then
|
||||
has_k9s=$(type -P k9s)
|
||||
num_version="0"
|
||||
[ -n "$has_k9s" ] && k9s_version="$( k9s version | grep Version | cut -f2 -d"v" | sed 's/ //g')" && num_version=${k9s_version//\./}
|
||||
expected_version_num=${K9S_VERSION//\./}
|
||||
[ -z "$num_version" ] && num_version=0
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
mkdir -p k9s && cd k9s &&
|
||||
curl -fsSLO https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_${ORG_OS}_${ARCH}.tar.gz &&
|
||||
tar -xzf "k9s_${ORG_OS}_${ARCH}.tar.gz" &&
|
||||
sudo mv k9s /usr/local/bin &&
|
||||
cd "$ORG" && rm -rf /tmp/k9s "/k9s_${ORG_OS}_${ARCH}.tar.gz" &&
|
||||
printf "%s\t%s\n" "k9s" "installed $K9S_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "k9s" "$k9s_version" "expected $K9S_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "k9s" "already $K9S_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$AGE_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "age" ] ; then
|
||||
has_age=$(type -P age)
|
||||
num_version="0"
|
||||
[ -n "$has_age" ] && age_version="${AGE_VERSION}" && num_version=${age_version//\./}
|
||||
expected_version_num=${AGE_VERSION//\./}
|
||||
if [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
curl -fsSLO https://github.com/FiloSottile/age/releases/download/v${AGE_VERSION}/age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
|
||||
tar -xzf age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz &&
|
||||
sudo mv age/age /usr/local/bin &&
|
||||
sudo mv age/age-keygen /usr/local/bin &&
|
||||
rm -rf age "age-v${AGE_VERSION}-${OS}-${ARCH}.tar.gz" &&
|
||||
printf "%s\t%s\n" "age" "installed $AGE_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "age" "$age_version" "expected $AGE_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "age" "already $AGE_VERSION"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$SOPS_VERSION" ] && [ "$match" == "all" ] || [ "$match" == "sops" ] ; then
|
||||
has_sops=$(type -P sops)
|
||||
num_version="0"
|
||||
[ -n "$has_sops" ] && sops_version="$(sops -v | grep ^sops | cut -f2 -d" " | sed 's/ //g')" && num_version=${sops_version//\./}
|
||||
expected_version_num=${SOPS_VERSION//\./}
|
||||
[ -z "$num_version" ] && num_version=0
|
||||
if [ -z "$expected_version_num" ] ; then
|
||||
printf "%s\t%s\t%s\n" "sops" "$sops_version" "expected $SOPS_VERSION"
|
||||
elif [ -z "$CHECK_ONLY" ] && [ "$num_version" -lt "$expected_version_num" ] ; then
|
||||
mkdir -p sops && cd sops &&
|
||||
curl -fsSLO https://github.com/getsops/sops/releases/download/v${SOPS_VERSION}/sops-v${SOPS_VERSION}.${OS}.${ARCH} &&
|
||||
mv sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
|
||||
chmod +x sops &&
|
||||
sudo mv sops /usr/local/bin &&
|
||||
rm -f sops-v${SOPS_VERSION}.${OS}.${ARCH} sops &&
|
||||
printf "%s\t%s\n" "sops" "installed $SOPS_VERSION"
|
||||
elif [ -n "$CHECK_ONLY" ] ; then
|
||||
printf "%s\t%s\t%s\n" "sops" "$sops_version" "expected $SOPS_VERSION"
|
||||
else
|
||||
printf "%s\t%s\n" "sops" "already $SOPS_VERSION"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function _detect_tool_version {
|
||||
local tool=$1
|
||||
|
||||
# Try to detect version using tool-specific commands
|
||||
case "$tool" in
|
||||
hcloud)
|
||||
hcloud version 2>/dev/null | grep -o 'hcloud [0-9.]*' | awk '{print $2}' || echo ""
|
||||
;;
|
||||
upctl)
|
||||
upctl version 2>/dev/null | grep -i "Version" | head -1 | sed 's/.*Version:\s*//' | sed 's/[[:space:]]*$//' || echo ""
|
||||
;;
|
||||
aws)
|
||||
aws --version 2>/dev/null | cut -d' ' -f1 | sed 's/aws-cli\///' || echo ""
|
||||
;;
|
||||
nu | nushell)
|
||||
nu -v 2>/dev/null | head -1 || echo ""
|
||||
;;
|
||||
nickel)
|
||||
nickel --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo ""
|
||||
;;
|
||||
sops)
|
||||
sops -v 2>/dev/null | head -1 | awk '{print $2}' || echo ""
|
||||
;;
|
||||
age)
|
||||
age --version 2>/dev/null | head -1 | sed 's/^v//' || echo ""
|
||||
;;
|
||||
k9s)
|
||||
k9s version 2>/dev/null | grep -o "Version\s*v[0-9.]*" | sed 's/.*v//' || echo ""
|
||||
;;
|
||||
*)
|
||||
# Fallback: try tool version
|
||||
$tool version 2>/dev/null | head -1 || echo ""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function _normalize_version {
|
||||
local version=$1
|
||||
# Remove any 'v' prefix and return just major.minor
|
||||
echo "$version" | sed 's/^v//' | cut -d. -f1-2
|
||||
}
|
||||
|
||||
function _should_install_tool {
|
||||
local tool=$1
|
||||
local target_version=$2
|
||||
local force_update=$3
|
||||
|
||||
# Always install if --update flag is used
|
||||
if [ "$force_update" = "yes" ] ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get installed version
|
||||
local installed_version=$(_detect_tool_version "$tool")
|
||||
|
||||
# If tool not installed, install it
|
||||
if [ -z "$installed_version" ] ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Normalize versions for comparison (handle 0.109.0 vs 0.109)
|
||||
local norm_installed=$(_normalize_version "$installed_version")
|
||||
local norm_target=$(_normalize_version "$target_version")
|
||||
|
||||
# If installed version equals target version, skip installation
|
||||
if [ "$norm_installed" = "$norm_target" ] ; then
|
||||
printf "%s\t%s\t%s\n" "$tool" "$installed_version" "already up to date"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If versions differ, install (update)
|
||||
return 0
|
||||
}
|
||||
|
||||
function _try_install_provider_tool {
|
||||
local tool=$1
|
||||
local options=$2
|
||||
local force_update=$3
|
||||
|
||||
# Look for the tool in provider nickel/version.ncl files (Nickel is single source of truth)
|
||||
for prov in $(ls $PROVIDERS_PATH 2>/dev/null | grep -v "^_" )
|
||||
do
|
||||
if [ -r "$PROVIDERS_PATH/$prov/nickel/version.ncl" ] ; then
|
||||
# Evaluate Nickel file to JSON and extract version data (single source of truth)
|
||||
local nickel_file="$PROVIDERS_PATH/$prov/nickel/version.ncl"
|
||||
local nickel_output=""
|
||||
local tool_version=""
|
||||
local tool_name=""
|
||||
|
||||
# Evaluate Nickel to JSON and capture output
|
||||
nickel_output=$(nickel export --format json "$nickel_file" 2>/dev/null)
|
||||
|
||||
# Extract tool name and version from JSON
|
||||
tool_name=$(echo "$nickel_output" | grep -o '"name": "[^"]*"' | head -1 | sed 's/"name": "//;s/"$//')
|
||||
tool_version=$(echo "$nickel_output" | grep -o '"current": "[^"]*"' | head -1 | sed 's/"current": "//;s/"$//')
|
||||
|
||||
# If this is the tool we're looking for
|
||||
if [ "$tool_name" == "$tool" ] && [ -n "$tool_version" ] ; then
|
||||
# Check if installation is needed
|
||||
if ! _should_install_tool "$tool" "$tool_version" "$force_update" ; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -x "$PROVIDERS_PATH/$prov/bin/install.sh" ] ; then
|
||||
# Set environment variables for the provider's install.sh script
|
||||
# Provider scripts have different interfaces, so set env vars for all of them
|
||||
if [ "$prov" = "upcloud" ] ; then
|
||||
# UpCloud expects: tool name as param, UPCLOUD_UPCTL_VERSION env var for version
|
||||
export UPCLOUD_UPCTL_VERSION="$tool_version"
|
||||
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_name" $options
|
||||
elif [ "$prov" = "hetzner" ] ; then
|
||||
# Hetzner expects: version as param (from nickel/version.ncl)
|
||||
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_version" $options
|
||||
elif [ "$prov" = "aws" ] ; then
|
||||
# AWS format - set env var and pass tool name
|
||||
export AWS_AWS_VERSION="$tool_version"
|
||||
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_name" $options
|
||||
else
|
||||
# Generic: try version as parameter first
|
||||
$PROVIDERS_PATH/$prov/bin/install.sh "$tool_version" $options
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
function _on_tools {
|
||||
local tools_list=$1
|
||||
shift # Remove first argument
|
||||
|
||||
# Check if --update flag is present
|
||||
local force_update="no"
|
||||
for arg in "$@"
|
||||
do
|
||||
if [ "$arg" = "--update" ] ; then
|
||||
force_update="yes"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
[ -z "$tools_list" ] || [[ "$tools_list" == -* ]] && tools_list=${TOOL_TO_INSTALL:-all}
|
||||
case $tools_list in
|
||||
"all")
|
||||
_install_tools "all" "$@"
|
||||
_install_providers "all" "$@"
|
||||
;;
|
||||
"providers" | "prov" | "p")
|
||||
_install_providers "$@"
|
||||
;;
|
||||
*)
|
||||
for tool in $tools_list
|
||||
do
|
||||
[[ "$tool" == -* ]] && continue
|
||||
# First try to find and install as provider tool
|
||||
if _try_install_provider_tool "$tool" "" "$force_update" ; then
|
||||
continue
|
||||
fi
|
||||
# Otherwise try core system tools
|
||||
_install_tools "$tool" "$@"
|
||||
done
|
||||
esac
|
||||
}
|
||||
|
||||
set -o allexport
|
||||
## shellcheck disable=SC1090
|
||||
[ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV"
|
||||
[ -r "../env-provisioning" ] && source ../env-provisioning
|
||||
[ -r "env-provisioning" ] && source ./env-provisioning
|
||||
#[ -r ".env" ] && source .env set
|
||||
set +o allexport
|
||||
|
||||
export PROVISIONING=${PROVISIONING:-/usr/local/provisioning}
|
||||
|
||||
if [ -r "$(dirname "$0")/../versions" ] ; then
|
||||
. "$(dirname "$0")"/../versions
|
||||
elif [ -r "$(dirname "$0")/versions" ] ; then
|
||||
. "$(dirname "$0")"/versions
|
||||
fi
|
||||
|
||||
export CMDS_PROVISIONING=${CMDS_PROVISIONING:-"tree"}
|
||||
PROVIDERS_PATH=${PROVIDERS_PATH:-"$PROVISIONING/extensions/providers"}
|
||||
|
||||
if [ -z "$1" ] ; then
|
||||
CHECK_ONLY="yes"
|
||||
_on_tools all
|
||||
else
|
||||
[ "$1" == "-h" ] && echo "$USAGE" && shift
|
||||
[ "$1" == "check" ] && CHECK_ONLY="yes" && shift
|
||||
[ -n "$1" ] && cd /tmp && _on_tools "$@"
|
||||
fi
|
||||
exit 0
|
||||
28
cli/tty-commands.conf
Normal file
28
cli/tty-commands.conf
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Minimalist TTY Command Registry (Nu-based)
|
||||
# Format: "COMMAND_PATTERN" "DISPATCHER_CALL" "FLOW_TYPE"
|
||||
# All commands routed through tty-dispatch.sh → Nu functions
|
||||
# Flow types: "exit" (standalone), "pipe" (inter-command), "continue" (to Nushell)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Authentication & Setup Commands
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Standalone wizards (flow=exit)
|
||||
"setup wizard" "core/cli/tty-dispatch.sh setup-wizard exit" "exit"
|
||||
"auth login" "core/cli/tty-dispatch.sh login exit" "exit"
|
||||
"auth mfa enroll" "core/cli/tty-dispatch.sh mfa-enroll exit" "exit"
|
||||
|
||||
# Pipeline commands (flow=pipe) - output to stdout
|
||||
"auth get-key" "core/cli/tty-dispatch.sh get-key pipe" "pipe"
|
||||
|
||||
# Continue to Nushell (flow=continue) - output captured in $TTY_OUTPUT
|
||||
"auth integrate" "core/cli/tty-dispatch.sh credential-input continue" "continue"
|
||||
"secret configure" "core/cli/tty-dispatch.sh secret-configure continue" "continue"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Future-proofing: Add new commands without modifying tty-filter.sh
|
||||
# Example:
|
||||
# "wizard something" "core/cli/tty-dispatch.sh something exit" "exit"
|
||||
# "get something" "core/cli/tty-dispatch.sh something pipe" "pipe"
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
86
cli/tty-dispatch.sh
Executable file
86
cli/tty-dispatch.sh
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env bash
|
||||
# Universal TTY Command Dispatcher
|
||||
# Routes TTY commands to Nu functions with proper output handling
|
||||
# Usage: tty-dispatch.sh <function-name> [flow-type] [args...]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FUNCTION_NAME="${1:-}"
|
||||
FLOW_TYPE="${2:-exit}"
|
||||
shift 2 || true
|
||||
|
||||
if [[ -z "$FUNCTION_NAME" ]]; then
|
||||
echo "Error: Function name required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find nu binary
|
||||
NU=$(type -P nu 2>/dev/null || echo "")
|
||||
if [[ -z "$NU" ]]; then
|
||||
echo "Error: nu not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get provisioning root
|
||||
PROVISIONING="${PROVISIONING:-/usr/local/provisioning}"
|
||||
|
||||
# Map function name to Nu function with proper naming conventions
|
||||
case "$FUNCTION_NAME" in
|
||||
"setup-wizard")
|
||||
NU_FUNCTION="run-setup-wizard-interactive"
|
||||
;;
|
||||
"login"|"auth-login")
|
||||
NU_FUNCTION="login-interactive"
|
||||
;;
|
||||
"mfa"|"mfa-enroll"|"auth-mfa-enroll")
|
||||
NU_FUNCTION="mfa-enroll-interactive"
|
||||
;;
|
||||
"auth-get-key"|"get-key")
|
||||
NU_FUNCTION="get-api-key-interactive"
|
||||
;;
|
||||
"auth-integrate"|"credential-input")
|
||||
NU_FUNCTION="get-provider-credentials-interactive"
|
||||
;;
|
||||
"secret-configure")
|
||||
NU_FUNCTION="get-secret-config-interactive"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown function: $FUNCTION_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Execute Nu function with proper output handling
|
||||
case "$FLOW_TYPE" in
|
||||
"exit")
|
||||
# Standalone: Execute and exit immediately
|
||||
$NU -n -c "
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
|
||||
$NU_FUNCTION
|
||||
"
|
||||
exit $?
|
||||
;;
|
||||
"pipe")
|
||||
# Pipeline: Output to stdout for piping
|
||||
$NU -n -c "
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
|
||||
$NU_FUNCTION
|
||||
"
|
||||
exit $?
|
||||
;;
|
||||
"continue")
|
||||
# Continue to Nushell: Output as JSON for $TTY_OUTPUT
|
||||
$NU -n -c "
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/plugins/auth.nu' *
|
||||
use '$PROVISIONING/core/nulib/lib_provisioning/setup/wizard.nu' *
|
||||
$NU_FUNCTION | to json
|
||||
"
|
||||
exit $?
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown flow type: $FLOW_TYPE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
137
cli/tty-filter.sh
Executable file
137
cli/tty-filter.sh
Executable file
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/bash
|
||||
# Description: Flow-Aware TTY Command Filter
|
||||
# Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell)
|
||||
# Arguments: $@ - Command and arguments
|
||||
# Returns: 0 if TTY command handled with flow=continue (continue to Nushell)
|
||||
# Exits with wrapper code for flow=exit or flow=pipe
|
||||
# 1 if not a TTY command (continue to normal processing)
|
||||
# Output: Exports TTY_OUTPUT and PROVISIONING_BYPASS_DAEMON on flow=continue
|
||||
|
||||
# Only apply strict mode when run standalone — sourcing this file must not
|
||||
# contaminate the calling shell's options (set -e would cause `DAEMON_OUTPUT=$(curl ...)`
|
||||
# to exit the parent script with curl's non-zero exit code instead of falling through).
|
||||
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
|
||||
|
||||
# Description: Check if command matches TTY pattern and manage flow
|
||||
# Arguments: $@ - Full command line
|
||||
# Returns: 0 for flow=continue (don't exit), non-zero for error/not-matched
|
||||
# Exits for flow=exit or flow=pipe (calls exit)
|
||||
# Output: Executes wrapper or exports environment
|
||||
filter_tty_command() {
|
||||
local cmd="$*"
|
||||
local registry_file="${PROVISIONING:-}/core/cli/tty-commands.conf"
|
||||
|
||||
# Validate registry exists
|
||||
if [[ ! -f "$registry_file" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Read registry using separate file descriptor to preserve stdin
|
||||
while IFS= read -r line <&3 || [[ -n "$line" ]]; do
|
||||
# Skip comments and separators
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*═ ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
|
||||
# Parse three-field format: "PATTERN" "WRAPPER" "FLOW_TYPE"
|
||||
if [[ "$line" =~ ^\"([^\"]+)\"[[:space:]]+\"([^\"]+)\"[[:space:]]+\"([^\"]+)\" ]]; then
|
||||
local pattern="${BASH_REMATCH[1]}"
|
||||
local wrapper="${BASH_REMATCH[2]}"
|
||||
local flow_type="${BASH_REMATCH[3]}"
|
||||
|
||||
# Check if command starts with pattern (prefix match)
|
||||
# This allows commands with additional arguments like "auth integrate --provider azure"
|
||||
if [[ "$cmd" == "$pattern"* ]]; then
|
||||
local wrapper_path="${PROVISIONING}/${wrapper}"
|
||||
|
||||
# Validate wrapper exists and is executable
|
||||
if [[ ! -x "$wrapper_path" ]]; then
|
||||
echo "Warning: TTY wrapper not found or not executable: $wrapper_path" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract arguments after pattern
|
||||
# Pattern may be multi-word (e.g., "setup platform")
|
||||
# Count pattern words and skip them from arguments
|
||||
local pattern_words=($pattern)
|
||||
local pattern_count=${#pattern_words[@]}
|
||||
local wrapper_args=()
|
||||
|
||||
# Shift arguments to skip pattern words
|
||||
for ((i=pattern_count; i<$#; i++)); do
|
||||
wrapper_args+=("${@:i+1:1}")
|
||||
done
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FLOW TYPE: exit (standalone TTY)
|
||||
# Execute wrapper and exit immediately
|
||||
# Never reaches Nushell dispatcher
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
if [[ "$flow_type" == "exit" ]]; then
|
||||
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
|
||||
bash "$wrapper_path" "${wrapper_args[@]}"
|
||||
else
|
||||
bash "$wrapper_path"
|
||||
fi
|
||||
exit $?
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FLOW TYPE: pipe (inter-command piping)
|
||||
# Execute wrapper, output to stdout, exit
|
||||
# Allows piping to next command in pipeline
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
elif [[ "$flow_type" == "pipe" ]]; then
|
||||
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
|
||||
bash "$wrapper_path" "${wrapper_args[@]}"
|
||||
else
|
||||
bash "$wrapper_path"
|
||||
fi
|
||||
exit $?
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FLOW TYPE: continue (same-command Nushell processing)
|
||||
# Execute wrapper, capture output, continue to Nushell
|
||||
# Nushell receives $env.TTY_OUTPUT and original args
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
elif [[ "$flow_type" == "continue" ]]; then
|
||||
# Execute wrapper and capture output
|
||||
local tty_output
|
||||
if [[ ${#wrapper_args[@]} -gt 0 ]]; then
|
||||
tty_output=$(bash "$wrapper_path" "${wrapper_args[@]}" 2>&1) || {
|
||||
local exit_code=$?
|
||||
echo "Error: TTY wrapper failed with code $exit_code" >&2
|
||||
echo "$tty_output" >&2
|
||||
exit $exit_code
|
||||
}
|
||||
else
|
||||
tty_output=$(bash "$wrapper_path" 2>&1) || {
|
||||
local exit_code=$?
|
||||
echo "Error: TTY wrapper failed with code $exit_code" >&2
|
||||
echo "$tty_output" >&2
|
||||
exit $exit_code
|
||||
}
|
||||
fi
|
||||
|
||||
# Export output for Nushell scripts to access
|
||||
export TTY_OUTPUT="$tty_output"
|
||||
export PROVISIONING_BYPASS_DAEMON="true"
|
||||
export TTY_WRAPPER_EXECUTED="true"
|
||||
|
||||
# Return 0 WITHOUT exiting - allows continuation to Nushell
|
||||
return 0
|
||||
|
||||
else
|
||||
echo "Warning: Unknown flow type '$flow_type' for pattern '$pattern'" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done 3< "$registry_file"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Only run filter if called directly (not sourced)
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
filter_tty_command "$@"
|
||||
fi
|
||||
374
nulib/_bootstrap/env.nu
Normal file
374
nulib/_bootstrap/env.nu
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
use std
|
||||
use platform/config/accessor.nu *
|
||||
export-env {
|
||||
# Detect active workspace BEFORE loading config. Cross-platform user config
|
||||
# path resolution is inlined here because this export-env runs before most
|
||||
# modules are available for import.
|
||||
let active_workspace = do {
|
||||
let override = ($env.PROVISIONING_USER_CONFIG_DIR? | default "")
|
||||
let home = ($env.HOME? | default "")
|
||||
let user_config_dir = if ($override | is-not-empty) { $override } else {
|
||||
match $nu.os-info.name {
|
||||
"macos" => ([$home "Library" "Application Support" "provisioning"] | path join)
|
||||
"linux" => {
|
||||
let xdg = ($env.XDG_CONFIG_HOME? | default "")
|
||||
if ($xdg | is-not-empty) { ($xdg | path join "provisioning") } else {
|
||||
([$home ".config" "provisioning"] | path join)
|
||||
}
|
||||
}
|
||||
"windows" => {
|
||||
let appdata = ($env.APPDATA? | default "")
|
||||
if ($appdata | is-not-empty) { ($appdata | path join "provisioning") } else {
|
||||
([$home "AppData" "Roaming" "provisioning"] | path join)
|
||||
}
|
||||
}
|
||||
_ => ([$home ".config" "provisioning"] | path join)
|
||||
}
|
||||
}
|
||||
let user_config_path = ($user_config_dir | path join "user_config.yaml")
|
||||
|
||||
if ($user_config_path | path exists) {
|
||||
let user_config = (open $user_config_path)
|
||||
if ($user_config.active_workspace != null) {
|
||||
let workspace_name = $user_config.active_workspace
|
||||
let workspaces = ($user_config.workspaces | where name == $workspace_name)
|
||||
if ($workspaces | length) > 0 {
|
||||
($workspaces | first).path
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
$env.PROVISIONING_KLOUD_PATH = if ($active_workspace | is-not-empty) {
|
||||
$active_workspace
|
||||
} else {
|
||||
($env.PROVISIONING_KLOUD_PATH? | default "")
|
||||
}
|
||||
|
||||
# Don't load config during export-env to avoid hanging on module parsing
|
||||
# Config will be loaded on-demand when accessed later
|
||||
let config = {}
|
||||
|
||||
# Try to get PROVISIONING path from config, environment, or detect from project structure
|
||||
let provisioning_from_config = (config-get "provisioning.path" "" --config $config)
|
||||
let provisioning_from_env = ($env.PROVISIONING? | default "")
|
||||
|
||||
# Detect project root if not already configured
|
||||
let potential_roots = [
|
||||
($env.PWD)
|
||||
(if ($provisioning_from_env | is-not-empty) { $provisioning_from_env } else { "" })
|
||||
(if ($provisioning_from_config | is-not-empty) { $provisioning_from_config } else { "" })
|
||||
"/usr/local/provisioning"
|
||||
]
|
||||
|
||||
let detected_root = ($potential_roots
|
||||
| where { |path| ($path | path join "core" "nulib" | path exists) }
|
||||
| first
|
||||
| default "/usr/local/provisioning")
|
||||
|
||||
$env.PROVISIONING = if ($provisioning_from_config | is-not-empty) {
|
||||
$provisioning_from_config
|
||||
} else if ($provisioning_from_env | is-not-empty) {
|
||||
$provisioning_from_env
|
||||
} else {
|
||||
$detected_root
|
||||
}
|
||||
|
||||
$env.PROVISIONING_CORE = ($env.PROVISIONING | path join "core")
|
||||
if ($env.PROVISIONING_CORE | path exists) == false {
|
||||
# For workspace-exempt commands, we don't need valid paths - skip validation
|
||||
# The workspace enforcement will catch commands that actually need workspace
|
||||
# Just set it to a reasonable default
|
||||
$env.PROVISIONING_CORE = "/usr/local/provisioning/core"
|
||||
}
|
||||
$env.PROVISIONING_PROVIDERS_PATH = ($env.PROVISIONING | path join "catalog" | path join "providers")
|
||||
$env.PROVISIONING_COMPONENTS_PATH = ($env.PROVISIONING | path join "catalog" | path join "components")
|
||||
# Keep for backward compat — points to taskservs/ if it exists, falls back to components/
|
||||
let _ts_path = ($env.PROVISIONING | path join "catalog" | path join "taskservs")
|
||||
$env.PROVISIONING_TASKSERVS_PATH = if ($env.PROVISIONING_COMPONENTS_PATH | path exists) {
|
||||
$env.PROVISIONING_COMPONENTS_PATH
|
||||
} else {
|
||||
$_ts_path
|
||||
}
|
||||
$env.PROVISIONING_CLUSTERS_PATH = ($env.PROVISIONING | path join "catalog" | path join "_clusters")
|
||||
$env.PROVISIONING_RESOURCES = ($env.PROVISIONING | path join "resources" )
|
||||
$env.PROVISIONING_NOTIFY_ICON = ($env.PROVISIONING_RESOURCES | path join "images"| path join "cloudnative.png")
|
||||
|
||||
$env.PROVISIONING_DEBUG = (config-get "debug.enabled" false --config $config)
|
||||
$env.PROVISIONING_METADATA = (config-get "debug.metadata" false --config $config)
|
||||
|
||||
$env.PROVISIONING_DEBUG_CHECK = (config-get "debug.check" false --config $config)
|
||||
$env.PROVISIONING_DEBUG_REMOTE = (config-get "debug.remote" false --config $config)
|
||||
$env.PROVISIONING_LOG_LEVEL = (config-get "debug.log_level" "" --config $config)
|
||||
|
||||
$env.PROVISIONING_NO_TERMINAL = (config-get "debug.no_terminal" false --config $config)
|
||||
# Only set NO_TITLES from config if not already set via environment
|
||||
let no_titles_env = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
$env.PROVISIONING_NO_TITLES = if ($no_titles_env | is-not-empty) {
|
||||
($no_titles_env == "true" or $no_titles_env == "1" or $no_titles_env == true)
|
||||
} else {
|
||||
(config-get "debug.no_titles" false --config $config)
|
||||
}
|
||||
$env.PROVISIONING_ARGS = ($env.PROVISIONING_ARGS? | default "")
|
||||
$env.PROVISIONING_MODULE = ($env.PROVISIONING_MODULE? | default "")
|
||||
$env.PROVISIONING_NAME = (config-get "core.name" "provisioning" --config $config)
|
||||
|
||||
$env.PROVISIONING_FILEVIEWER = (config-get "output.file_viewer" "bat" --config $config)
|
||||
|
||||
$env.PROVISIONING_METADATA = if ($env.PROVISIONING_ARGS? | str contains "--xm" ) { true } else { $env.PROVISIONING_METADATA }
|
||||
$env.PROVISIONING_DEBUG_CHECK = if ($env.PROVISIONING_ARGS? | str contains "--xc" ) { true } else { $env.PROVISIONING_DEBUG_CHECK }
|
||||
$env.PROVISIONING_DEBUG_REMOTE = if ($env.PROVISIONING_ARGS? | str contains "--xr" ) { true } else { $env.PROVISIONING_DEBUG_REMOTE }
|
||||
$env.PROVISIONING_LOG_LEVEL = if ($env.PROVISIONING_ARGS? | str contains "--xld" ) { "debug" } else { $env.PROVISIONING_LOG_LEVEL }
|
||||
|
||||
if $env.PROVISIONING_LOG_LEVEL == "debug" or $env.PROVISIONING_LOG_LEVEL == "DEBUG" { $env.NU_LOG_LEVEL = "DEBUG" } else { $env.NU_LOG_LEVEL = ""}
|
||||
|
||||
$env.PROVISIONING_INFRA_PATH = ($env.PROVISIONING_KLOUD_PATH? | default
|
||||
(config-get "paths.infra" | default $env.PWD ) | into string)
|
||||
|
||||
$env.PROVISIONING_DFLT_SET = (config-get "paths.files.settings" | default "settings.ncl" | into string)
|
||||
|
||||
$env.NOW = (date now | format date "%Y_%m_%d_%H_%M_%S")
|
||||
$env.PROVISIONING_MATCH_DATE = ($env.PROVISIONING_MATCH_DATE? | default "%Y_%m")
|
||||
|
||||
#$env.PROVISIONING_MATCH_CMD = "v"
|
||||
|
||||
$env.PROVISIONING_WK_FORMAT = (config-get "output.format" | default "yaml" | into string)
|
||||
|
||||
$env.PROVISIONING_REQ_VERSIONS = ($env.PROVISIONING | path join "core" | path join "versions.yaml")
|
||||
$env.PROVISIONING_TOOLS_PATH = ($env.PROVISIONING | path join "core" | path join "tools")
|
||||
$env.PROVISIONING_TEMPLATES_PATH = ($env.PROVISIONING | path join "templates")
|
||||
$env.SSH_OPS = [StrictHostKeyChecking=accept-new UserKnownHostsFile=(if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" })]
|
||||
|
||||
# Path for cloud local tasks definition can not exist if all tasks are using library install mode from 'lib-tasks'
|
||||
$env.PROVISIONING_RUN_TASKSERVS_PATH = "taskservs"
|
||||
$env.PROVISIONING_RUN_CLUSTERS_PATH = "clusters"
|
||||
$env.PROVISIONING_GENERATE_DIRPATH = "generate"
|
||||
$env.PROVISIONING_GENERATE_DEFSFILE = "defs.toml"
|
||||
|
||||
$env.PROVISIONING_KEYS_PATH = (config-get "paths.files.keys" ".keys.ncl" --config $config)
|
||||
|
||||
$env.PROVISIONING_USE_NICKEL_PLUGIN = if ( (version).installed_plugins | str contains "nickel" ) { true } else { false }
|
||||
#$env.PROVISIONING_J2_PARSER = ($env.PROVISIONING_$TOOLS_PATH | path join "parsetemplate.py")
|
||||
#$env.PROVISIONING_J2_PARSER = (^bash -c "type -P tera")
|
||||
$env.PROVISIONING_USE_TERA_PLUGIN = if ( (version).installed_plugins | str contains "tera" ) { true } else { false }
|
||||
|
||||
# Provisioning critical plugins (10-30x performance improvement)
|
||||
# These plugins provide native Rust performance for auth, KMS, and orchestrator operations
|
||||
let installed_plugins = ((version).installed_plugins | default "")
|
||||
$env.PROVISIONING_USE_AUTH_PLUGIN = ($installed_plugins | str contains "nu_plugin_auth")
|
||||
$env.PROVISIONING_USE_KMS_PLUGIN = ($installed_plugins | str contains "nu_plugin_kms")
|
||||
$env.PROVISIONING_USE_ORCH_PLUGIN = ($installed_plugins | str contains "nu_plugin_orchestrator")
|
||||
|
||||
# Combined plugin availability flag
|
||||
$env.PROVISIONING_PLUGINS_AVAILABLE = ($env.PROVISIONING_USE_AUTH_PLUGIN
|
||||
or $env.PROVISIONING_USE_KMS_PLUGIN
|
||||
or $env.PROVISIONING_USE_ORCH_PLUGIN)
|
||||
|
||||
# Plugin status check (variables set, but don't warn unless explicitly requested)
|
||||
# Users will be notified only if they try to use a plugin that's not available
|
||||
# This keeps the interactive experience clean while still supporting fallback to HTTP
|
||||
|
||||
$env.PROVISIONING_URL = ($env.PROVISIONING_URL? | default "https://provisioning.systems" | into string)
|
||||
# Refactored from try-catch to do/complete for explicit error handling
|
||||
#let parts_k = (do { $env.PROVISIONING_ARGS | split row "-k" | get 1 } | complete)
|
||||
#let infra = if $parts_k.exit_code == 0 {
|
||||
# ($parts_k.stdout | str trim)
|
||||
#} else {
|
||||
# let parts_space = (do { $env.PROVISIONING_ARGS | split row " " | get 1 } | complete)
|
||||
# if $parts_space.exit_code == 0 { ($parts_space.stdout | str trim) } else { "" }
|
||||
#}
|
||||
#$env.CURR_KLOUD = if $infra == "" { (^pwd) } else { $infra }
|
||||
|
||||
$env.PROVISIONING_USE_SOPS = (config-get "sops.use_sops" | default "age" | into string)
|
||||
$env.PROVISIONING_USE_KMS = (config-get "sops.use_kms" | default "" | into string)
|
||||
$env.PROVISIONING_SECRET_PROVIDER = (config-get "sops.secret_provider" | default "sops" | into string)
|
||||
|
||||
# AI Configuration
|
||||
$env.PROVISIONING_AI_ENABLED = (config-get "ai.enabled" | default false | into bool | into string)
|
||||
$env.PROVISIONING_AI_PROVIDER = (config-get "ai.provider" | default "openai" | into string)
|
||||
$env.PROVISIONING_LAST_ERROR = ""
|
||||
|
||||
# CLI Daemon Configuration
|
||||
$env.PROVISIONING_DAEMON_URL = ($env.PROVISIONING_DAEMON_URL? | default "http://localhost:9091" | into string)
|
||||
|
||||
|
||||
# For SOPS if settings below fails -> look at: sops_env.nu loaded when is need to set env context
|
||||
let curr_infra = (config-get "paths.infra" "" --config $config)
|
||||
if $curr_infra != "" { $env.CURRENT_INFRA_PATH = $curr_infra }
|
||||
|
||||
let sops_path = (config-get "sops.config_path" | default "" | str replace "KLOUD_PATH" $env.PROVISIONING_KLOUD_PATH)
|
||||
if $sops_path != "" {
|
||||
$env.PROVISIONING_SOPS = $sops_path
|
||||
} else if $env.CURRENT_KLOUD_PATH? != null and ($env.CURRENT_INFRA_PATH | is -not-empty) {
|
||||
$env.PROVISIONING_SOPS = (get_def_sops $env.CURRENT_KLOUD_PATH)
|
||||
}
|
||||
|
||||
let kage_path = (config-get "sops.key_path" | default "" | str replace "KLOUD_PATH" $env.PROVISIONING_KLOUD_PATH)
|
||||
if $kage_path != "" {
|
||||
$env.PROVISIONING_KAGE = $kage_path
|
||||
} else if $env.CURRENT_KLOUD_PATH? != null and ($env.CURRENT_INFRA_PATH | is-not-empty) {
|
||||
$env.PROVISIONING_KAGE = (get_def_age $env.CURRENT_KLOUD_PATH)
|
||||
}
|
||||
|
||||
if $env.PROVISIONING_KAGE? != null and ($env.PROVISIONING_KAGE | is-not-empty) {
|
||||
$env.SOPS_AGE_KEY_FILE = $env.PROVISIONING_KAGE
|
||||
$env.SOPS_AGE_RECIPIENTS = (grep "public key:" $env.SOPS_AGE_KEY_FILE | split row ":" |
|
||||
get -o 1 | str trim | default "")
|
||||
if $env.SOPS_AGE_RECIPIENTS == "" {
|
||||
print $"❗Error no key found in (_ansi red_bold)($env.SOPS_AGE_KEY_FILE)(_ansi reset) file for secure AGE operations "
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
$env.PROVISIONING_OUT = ($env.PROVISIONING_OUT? | default "")
|
||||
if ($env.PROVISIONING_OUT | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TERMINAL = true
|
||||
# if ($env.PROVISIONING_OUT | str ends-with ".yaml") or ($env.PROVISIONING_OUT | str ends-with ".yml") {
|
||||
# $env.PROVISIONING_NO_TERMINAL = true
|
||||
# } else if ($env.PROVISIONING_OUT | str ends-with ".json") {
|
||||
# $env.PROVISIONING_NO_TERMINAL = true
|
||||
# } else {
|
||||
# $env.PROVISIONING_NO_TERMINAL = true
|
||||
# }
|
||||
}
|
||||
# Nickel Module Path Configuration
|
||||
# Set up NICKEL_IMPORT_PATH to help Nickel resolve modules when running from different directories
|
||||
$env.NICKEL_IMPORT_PATH = ($env.NICKEL_IMPORT_PATH? | default [] | append [
|
||||
$env.PROVISIONING
|
||||
($env.PROVISIONING | path join "nickel")
|
||||
($env.PROVISIONING_PROVIDERS_PATH)
|
||||
$env.PWD
|
||||
] | uniq | str join ":")
|
||||
|
||||
# Path helpers for dynamic imports
|
||||
$env.PROVISIONING_CORE_NULIB = ($env.PROVISIONING | path join "core" "nulib")
|
||||
$env.PROVISIONING_PROV_LIB = ($env.PROVISIONING_PROVIDERS_PATH | path join "prov_lib")
|
||||
|
||||
# Add extensions paths to NU_LIB_DIRS for module discovery
|
||||
$env.NU_LIB_DIRS = ($env.NU_LIB_DIRS? | default [] | append [
|
||||
$env.PROVISIONING_PROVIDERS_PATH
|
||||
$env.PROVISIONING_TASKSERVS_PATH
|
||||
$env.PROVISIONING_CLUSTERS_PATH
|
||||
($env.PROVISIONING | path join "catalog")
|
||||
$env.PROVISIONING_CORE_NULIB
|
||||
] | uniq)
|
||||
|
||||
# Extension System Configuration
|
||||
$env.PROVISIONING_EXTENSIONS_PATH = ($env.PROVISIONING_EXTENSIONS_PATH? | default
|
||||
(config-get "extensions.path" | default "") | into string)
|
||||
|
||||
$env.PROVISIONING_EXTENSION_MODE = ($env.PROVISIONING_EXTENSION_MODE? | default
|
||||
(config-get "extensions.mode" | default "full") | into string)
|
||||
|
||||
$env.PROVISIONING_PROFILE = ($env.PROVISIONING_PROFILE? | default
|
||||
(config-get "extensions.profile" | default "") | into string)
|
||||
|
||||
$env.PROVISIONING_ALLOWED_EXTENSIONS = ($env.PROVISIONING_ALLOWED_EXTENSIONS? | default
|
||||
(config-get "extensions.allowed" | default "") | into string)
|
||||
|
||||
$env.PROVISIONING_BLOCKED_EXTENSIONS = ($env.PROVISIONING_BLOCKED_EXTENSIONS? | default
|
||||
(config-get "extensions.blocked" | default "") | into string)
|
||||
|
||||
# Custom paths for extensions
|
||||
$env.PROVISIONING_CUSTOM_PROVIDERS = ($env.PROVISIONING_CUSTOM_PROVIDERS? | default "" | into string)
|
||||
$env.PROVISIONING_CUSTOM_TASKSERVS = ($env.PROVISIONING_CUSTOM_TASKSERVS? | default "" | into string)
|
||||
|
||||
# Project-local environment should be loaded manually if needed
|
||||
# Example: source .env.nu (from project directory)
|
||||
|
||||
# Load providers environment settings...
|
||||
# use ../../providers/prov_lib/env_middleware.nu
|
||||
|
||||
# Auto-load tera plugin if available for template rendering at env initialization
|
||||
# Call this in a block that runs AFTER the export-env completes
|
||||
if ( (version).installed_plugins | str contains "tera" ) {
|
||||
(plugin use tera)
|
||||
}
|
||||
}
|
||||
|
||||
export def "show_env" [
|
||||
] {
|
||||
let env_vars = {
|
||||
PROVISIONING: $env.PROVISIONING,
|
||||
PROVISIONING_CORE: $env.PROVISIONING_CORE,
|
||||
PROVISIONING_PROVIDERS_PATH: $env.PROVISIONING_PROVIDERS_PATH,
|
||||
PROVISIONING_TASKSERVS_PATH: $env.PROVISIONING_TASKSERVS_PATH,
|
||||
PROVISIONING_CLUSTERS_PATH: $env.PROVISIONING_CLUSTERS_PATH,
|
||||
PROVISIONING_RESOURCES: $env.PROVISIONING_RESOURCES,
|
||||
PROVISIONING_NOTIFY_ICON: $env.PROVISIONING_NOTIFY_ICON,
|
||||
|
||||
PROVISIONING_DEBUG: $"($env.PROVISIONING_DEBUG)",
|
||||
PROVISIONING_METADATA: $"($env.PROVISIONING_METADATA)",
|
||||
|
||||
PROVISIONING_DEBUG_CHECK: $"($env.PROVISIONING_DEBUG_CHECK)",
|
||||
PROVISIONING_DEBUG_REMOTE: $"($env.PROVISIONING_DEBUG_REMOTE)",
|
||||
PROVISIONING_LOG_LEVEL: $env.PROVISIONING_LOG_LEVEL,
|
||||
|
||||
PROVISIONING_NO_TERMINAL: $env.PROVISIONING_NO_TERMINAL,
|
||||
PROVISIONING_ARGS: $env.PROVISIONING_ARGS,
|
||||
PROVISIONING_MODULE: $env.PROVISIONING_MODULE,
|
||||
PROVISIONING_NAME: $env.PROVISIONING_NAME,
|
||||
|
||||
PROVISIONING_FILEVIEWER: $env.PROVISIONING_FILEVIEWER,
|
||||
|
||||
NU_LOG_LEVEL: ($env.NU_LOG_LEVEL| default null),
|
||||
NU_LIB_DIRS: (if ($env.PROVISIONING_OUT | is-empty) { $env.NU_LIB_DIRS } else { $"($env.NU_LIB_DIRS | to json)"}),
|
||||
|
||||
PROVISIONING_KLOUD_PATH: $env.PROVISIONING_KLOUD_PATH,
|
||||
|
||||
PROVISIONING_DFLT_SET: $env.PROVISIONING_DFLT_SET,
|
||||
|
||||
NOW: $env.NOW,
|
||||
PROVISIONING_MATCH_DATE: $env.PROVISIONING_MATCH_DATE,
|
||||
|
||||
PROVISIONING_WK_FORMAT: $env.PROVISIONING_WK_FORMAT,
|
||||
|
||||
PROVISIONING_REQ_VERSIONS: $env.PROVISIONING_REQ_VERSIONS,
|
||||
PROVISIONING_TOOLS_PATH: $env.PROVISIONING_TOOLS_PATH,
|
||||
PROVISIONING_TEMPLATES_PATH: $env.PROVISIONING_TEMPLATES_PATH,
|
||||
SSH_OPS: (if ($env.PROVISIONING_OUT | is-empty) { $env.SSH_OPS } else { $"($env.SSH_OPS | to json)"}),
|
||||
|
||||
PROVISIONING_RUN_TASKSERVS_PATH: $env.PROVISIONING_RUN_TASKSERVS_PATH,
|
||||
PROVISIONING_RUN_CLUSTERS_PATH: $env.PROVISIONING_RUN_CLUSTERS_PATH,
|
||||
PROVISIONING_GENERATE_DIRPATH: $env.PROVISIONING_GENERATE_DIRPATH,
|
||||
PROVISIONING_GENERATE_DEFSFILE: $env.PROVISIONING_GENERATE_DEFSFILE,
|
||||
|
||||
PROVISIONING_KEYS_PATH: $env.PROVISIONING_KEYS_PATH,
|
||||
|
||||
PROVISIONING_USE_nickel: $"($env.PROVISIONING_USE_nickel)",
|
||||
PROVISIONING_J2_PARSER: ($env.PROVISIONING_J2_PARSER? | default ""),
|
||||
|
||||
PROVISIONING_URL: $env.PROVISIONING_URL,
|
||||
|
||||
PROVISIONING_USE_SOPS: $env.PROVISIONING_USE_SOPS,
|
||||
PROVISIONING_LAST_ERROR: $env.PROVISIONING_LAST_ERROR,
|
||||
|
||||
CURRENT_KLOUD_PATH: ($env.CURRENT_INFRA_PATH? | default ""),
|
||||
|
||||
PROVISIONING_SOPS: ($env.PROVISIONING_SOPS? | default ""),
|
||||
|
||||
PROVISIONING_KAGE: ($env.PROVISIONING_KAGE? | default ""),
|
||||
|
||||
PROVISIONING_OUT: $env.PROVISIONING_OUT,
|
||||
};
|
||||
|
||||
if $env.PROVISIONING_KAGE? != null and ($env.PROVISIONING_KAGE | is-not-empty) {
|
||||
$env_vars | merge {
|
||||
SOPS_AGE_KEY_FILE: $env.SOPS_AGE_KEY_FILE,
|
||||
SOPS_AGE_RECIPIENTS: $env.SOPS_AGE_RECIPIENTS,
|
||||
}
|
||||
} else {
|
||||
$env_vars
|
||||
}
|
||||
}
|
||||
|
||||
# Get CLI daemon URL for template rendering and other daemon operations
|
||||
# Returns the daemon endpoint, checking environment variable first, then default
|
||||
export def get-cli-daemon-url [] {
|
||||
$env.PROVISIONING_DAEMON_URL? | default "http://localhost:9091"
|
||||
}
|
||||
5
nulib/_bootstrap/mod.nu
Normal file
5
nulib/_bootstrap/mod.nu
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# _bootstrap/ subsystem façade (ADR-026).
|
||||
export use _bootstrap/env.nu [
|
||||
"show_env"
|
||||
get-cli-daemon-url
|
||||
]
|
||||
719
nulib/ai/query_processor.nu
Normal file
719
nulib/ai/query_processor.nu
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# AI Query Processing System
|
||||
# Enhanced natural language processing for infrastructure queries
|
||||
|
||||
# BROKEN: file not found — use ../observability/agents.nu *
|
||||
# BROKEN: file not found — use ../dataframes/polars_integration.nu *
|
||||
# BROKEN: file not found — use ../dataframes/log_processor.nu *
|
||||
|
||||
# Query types supported by the AI system
|
||||
const QUERY_TYPES = [
|
||||
"infrastructure_status"
|
||||
"performance_analysis"
|
||||
"cost_optimization"
|
||||
"security_audit"
|
||||
"predictive_analysis"
|
||||
"troubleshooting"
|
||||
"resource_planning"
|
||||
"compliance_check"
|
||||
]
|
||||
|
||||
# AI query processor
|
||||
export def process_query [
|
||||
query: string
|
||||
--context: string = "general"
|
||||
--agent: string = "auto"
|
||||
--format: string = "json"
|
||||
--max_results: int = 100
|
||||
] {
|
||||
|
||||
print $"🤖 Processing query: ($query)"
|
||||
|
||||
# Analyze query intent
|
||||
let query_analysis = analyze_query_intent $query
|
||||
let query_type = $query_analysis.type
|
||||
let entities = $query_analysis.entities
|
||||
let confidence = $query_analysis.confidence
|
||||
|
||||
print $"🎯 Query type: ($query_type) (confidence: ($confidence)%)"
|
||||
|
||||
# Select appropriate agent
|
||||
let selected_agent = if $agent == "auto" {
|
||||
select_optimal_agent $query_type $entities
|
||||
} else {
|
||||
$agent
|
||||
}
|
||||
|
||||
print $"🤖 Selected agent: ($selected_agent)"
|
||||
|
||||
# Process query with selected agent
|
||||
match $query_type {
|
||||
"infrastructure_status" => {
|
||||
process_infrastructure_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"performance_analysis" => {
|
||||
process_performance_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"cost_optimization" => {
|
||||
process_cost_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"security_audit" => {
|
||||
process_security_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"predictive_analysis" => {
|
||||
process_predictive_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"troubleshooting" => {
|
||||
process_troubleshooting_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"resource_planning" => {
|
||||
process_planning_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
"compliance_check" => {
|
||||
process_compliance_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
_ => {
|
||||
process_general_query $query $entities $selected_agent $format $max_results
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Analyze query intent using NLP patterns
|
||||
def analyze_query_intent [query: string] {
|
||||
let lower_query = ($query | str downcase)
|
||||
|
||||
# Infrastructure status patterns
|
||||
if ($lower_query | str contains "status") or ($lower_query | str contains "health") or ($lower_query | str contains "running") {
|
||||
return {
|
||||
type: "infrastructure_status"
|
||||
entities: (extract_entities $query ["servers", "services", "containers", "clusters"])
|
||||
confidence: 85
|
||||
keywords: ["status", "health", "running", "online", "offline"]
|
||||
}
|
||||
}
|
||||
|
||||
# Performance analysis patterns
|
||||
if ($lower_query | str contains "cpu") or ($lower_query | str contains "memory") or ($lower_query | str contains "performance") or ($lower_query | str contains "slow") {
|
||||
return {
|
||||
type: "performance_analysis"
|
||||
entities: (extract_entities $query ["servers", "applications", "services"])
|
||||
confidence: 90
|
||||
keywords: ["cpu", "memory", "performance", "slow", "fast", "usage"]
|
||||
}
|
||||
}
|
||||
|
||||
# Cost optimization patterns
|
||||
if ($lower_query | str contains "cost") or ($lower_query | str contains "expensive") or ($lower_query | str contains "optimize") or ($lower_query | str contains "save money") {
|
||||
return {
|
||||
type: "cost_optimization"
|
||||
entities: (extract_entities $query ["instances", "resources", "storage", "network"])
|
||||
confidence: 88
|
||||
keywords: ["cost", "expensive", "cheap", "optimize", "save", "money"]
|
||||
}
|
||||
}
|
||||
|
||||
# Security audit patterns
|
||||
if ($lower_query | str contains "security") or ($lower_query | str contains "vulnerability") or ($lower_query | str contains "threat") {
|
||||
return {
|
||||
type: "security_audit"
|
||||
entities: (extract_entities $query ["servers", "applications", "ports", "users"])
|
||||
confidence: 92
|
||||
keywords: ["security", "vulnerability", "threat", "breach", "attack"]
|
||||
}
|
||||
}
|
||||
|
||||
# Predictive analysis patterns
|
||||
if ($lower_query | str contains "predict") or ($lower_query | str contains "forecast") or ($lower_query | str contains "will") or ($lower_query | str contains "future") {
|
||||
return {
|
||||
type: "predictive_analysis"
|
||||
entities: (extract_entities $query ["capacity", "usage", "growth", "failures"])
|
||||
confidence: 80
|
||||
keywords: ["predict", "forecast", "future", "will", "trend"]
|
||||
}
|
||||
}
|
||||
|
||||
# Troubleshooting patterns
|
||||
if ($lower_query | str contains "error") or ($lower_query | str contains "problem") or ($lower_query | str contains "fail") or ($lower_query | str contains "issue") {
|
||||
return {
|
||||
type: "troubleshooting"
|
||||
entities: (extract_entities $query ["services", "logs", "errors", "applications"])
|
||||
confidence: 87
|
||||
keywords: ["error", "problem", "fail", "issue", "broken"]
|
||||
}
|
||||
}
|
||||
|
||||
# Default to general query
|
||||
{
|
||||
type: "general"
|
||||
entities: (extract_entities $query ["infrastructure", "system"])
|
||||
confidence: 60
|
||||
keywords: []
|
||||
}
|
||||
}
|
||||
|
||||
# Extract entities from query text
|
||||
def extract_entities [query: string, entity_types: list<string>] {
|
||||
let lower_query = ($query | str downcase)
|
||||
mut entities = []
|
||||
|
||||
# Infrastructure entities
|
||||
let infra_patterns = {
|
||||
servers: ["server", "instance", "vm", "machine", "host"]
|
||||
services: ["service", "application", "app", "microservice"]
|
||||
containers: ["container", "docker", "pod", "k8s", "kubernetes"]
|
||||
databases: ["database", "db", "mysql", "postgres", "mongodb"]
|
||||
network: ["network", "load balancer", "cdn", "dns"]
|
||||
storage: ["storage", "disk", "volume", "s3", "bucket"]
|
||||
}
|
||||
|
||||
for entity_type in $entity_types {
|
||||
if ($entity_type in ($infra_patterns | columns)) {
|
||||
let patterns = ($infra_patterns | get $entity_type)
|
||||
for pattern in $patterns {
|
||||
if ($lower_query | str contains $pattern) {
|
||||
$entities = ($entities | append $entity_type)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entities | uniq
|
||||
}
|
||||
|
||||
# Select optimal agent based on query type and entities
|
||||
def select_optimal_agent [query_type: string, entities: list<string>] {
|
||||
match $query_type {
|
||||
"infrastructure_status" => "infrastructure_monitor"
|
||||
"performance_analysis" => "performance_analyzer"
|
||||
"cost_optimization" => "cost_optimizer"
|
||||
"security_audit" => "security_monitor"
|
||||
"predictive_analysis" => "predictor"
|
||||
"troubleshooting" => "pattern_detector"
|
||||
"resource_planning" => "performance_analyzer"
|
||||
"compliance_check" => "security_monitor"
|
||||
_ => "pattern_detector"
|
||||
}
|
||||
}
|
||||
|
||||
# Process infrastructure status queries
|
||||
def process_infrastructure_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "🏗️ Analyzing infrastructure status..."
|
||||
|
||||
# Get infrastructure data
|
||||
let infra_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "status_check"
|
||||
include_metrics: true
|
||||
}
|
||||
|
||||
# Add current system metrics
|
||||
let current_metrics = collect_system_metrics
|
||||
let servers_status = get_servers_status
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "infrastructure_status"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
infrastructure: $infra_data
|
||||
metrics: $current_metrics
|
||||
servers: $servers_status
|
||||
}
|
||||
insights: (generate_infrastructure_insights $infra_data $current_metrics)
|
||||
recommendations: (generate_recommendations "infrastructure" $infra_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process performance analysis queries
|
||||
def process_performance_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "⚡ Analyzing performance metrics..."
|
||||
|
||||
# Get performance data from agent
|
||||
let perf_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "performance_analysis"
|
||||
time_range: "1h"
|
||||
}
|
||||
|
||||
# Get detailed metrics
|
||||
let cpu_data = collect_logs --sources ["system"] --since "1h" | query_dataframe $in "SELECT * FROM logs WHERE message LIKE '%CPU%'"
|
||||
let memory_data = collect_logs --sources ["system"] --since "1h" | query_dataframe $in "SELECT * FROM logs WHERE message LIKE '%memory%'"
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "performance_analysis"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
analysis: $perf_data
|
||||
cpu_usage: $cpu_data
|
||||
memory_usage: $memory_data
|
||||
bottlenecks: (identify_bottlenecks $perf_data)
|
||||
}
|
||||
insights: (generate_performance_insights $perf_data)
|
||||
recommendations: (generate_recommendations "performance" $perf_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process cost optimization queries
|
||||
def process_cost_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "💰 Analyzing cost optimization opportunities..."
|
||||
|
||||
let cost_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "cost_analysis"
|
||||
include_recommendations: true
|
||||
}
|
||||
|
||||
# Get resource utilization data
|
||||
let resource_usage = analyze_resource_utilization
|
||||
let cost_breakdown = get_cost_breakdown
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "cost_optimization"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
analysis: $cost_data
|
||||
resource_usage: $resource_usage
|
||||
cost_breakdown: $cost_breakdown
|
||||
optimization_opportunities: (identify_cost_savings $cost_data $resource_usage)
|
||||
}
|
||||
insights: (generate_cost_insights $cost_data)
|
||||
recommendations: (generate_recommendations "cost" $cost_data)
|
||||
potential_savings: (calculate_potential_savings $cost_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process security audit queries
|
||||
def process_security_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "🛡️ Performing security analysis..."
|
||||
|
||||
let security_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "security_audit"
|
||||
include_threats: true
|
||||
}
|
||||
|
||||
# Get security events and logs
|
||||
let security_logs = collect_logs --sources ["system"] --filter_level "warn" --since "24h"
|
||||
let failed_logins = query_dataframe $security_logs "SELECT * FROM logs WHERE message LIKE '%failed%' AND message LIKE '%login%'"
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "security_audit"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
analysis: $security_data
|
||||
security_logs: $security_logs
|
||||
failed_logins: $failed_logins
|
||||
vulnerabilities: (scan_vulnerabilities $security_data)
|
||||
compliance_status: (check_compliance $security_data)
|
||||
}
|
||||
insights: (generate_security_insights $security_data)
|
||||
recommendations: (generate_recommendations "security" $security_data)
|
||||
risk_score: (calculate_risk_score $security_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process predictive analysis queries
|
||||
def process_predictive_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "🔮 Generating predictive analysis..."
|
||||
|
||||
let prediction_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "predict"
|
||||
time_horizon: "30d"
|
||||
}
|
||||
|
||||
# Get historical data for predictions
|
||||
let historical_metrics = collect_logs --since "7d" --output_format "dataframe"
|
||||
let trend_analysis = time_series_analysis $historical_metrics --window "1d"
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "predictive_analysis"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
predictions: $prediction_data
|
||||
historical_data: $historical_metrics
|
||||
trends: $trend_analysis
|
||||
forecasts: (generate_forecasts $prediction_data $trend_analysis)
|
||||
}
|
||||
insights: (generate_predictive_insights $prediction_data)
|
||||
recommendations: (generate_recommendations "predictive" $prediction_data)
|
||||
confidence_score: (calculate_prediction_confidence $prediction_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process troubleshooting queries
|
||||
def process_troubleshooting_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "🔧 Analyzing troubleshooting data..."
|
||||
|
||||
let troubleshoot_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "troubleshoot"
|
||||
include_solutions: true
|
||||
}
|
||||
|
||||
# Get error logs and patterns
|
||||
let error_logs = collect_logs --filter_level "error" --since "1h"
|
||||
let error_patterns = analyze_logs $error_logs --analysis_type "patterns"
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "troubleshooting"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
analysis: $troubleshoot_data
|
||||
error_logs: $error_logs
|
||||
patterns: $error_patterns
|
||||
root_causes: (identify_root_causes $troubleshoot_data $error_patterns)
|
||||
solutions: (suggest_solutions $troubleshoot_data)
|
||||
}
|
||||
insights: (generate_troubleshooting_insights $troubleshoot_data)
|
||||
recommendations: (generate_recommendations "troubleshooting" $troubleshoot_data)
|
||||
urgency_level: (assess_urgency $troubleshoot_data)
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Process general queries
|
||||
def process_general_query [
|
||||
query: string
|
||||
entities: list<string>
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
] {
|
||||
|
||||
print "🤖 Processing general infrastructure query..."
|
||||
|
||||
let general_data = execute_agent $agent {
|
||||
query: $query
|
||||
entities: $entities
|
||||
operation: "general_analysis"
|
||||
}
|
||||
|
||||
let result = {
|
||||
query: $query
|
||||
type: "general"
|
||||
timestamp: (date now)
|
||||
data: {
|
||||
analysis: $general_data
|
||||
summary: (generate_general_summary $general_data)
|
||||
}
|
||||
insights: ["Query processed successfully", "Consider using more specific terms for better results"]
|
||||
recommendations: []
|
||||
}
|
||||
|
||||
format_response $result $format
|
||||
}
|
||||
|
||||
# Helper functions for data collection
|
||||
def collect_system_metrics [] {
|
||||
{
|
||||
cpu: (sys cpu | get cpu_usage | math avg)
|
||||
memory: (sys mem | get used)
|
||||
disk: (sys disks | get used | math sum)
|
||||
timestamp: (date now)
|
||||
}
|
||||
}
|
||||
|
||||
def get_servers_status [] {
|
||||
# Mock data - in real implementation would query actual infrastructure
|
||||
[
|
||||
{ name: "web-01", status: "healthy", cpu: 45, memory: 67 }
|
||||
{ name: "web-02", status: "healthy", cpu: 38, memory: 54 }
|
||||
{ name: "db-01", status: "warning", cpu: 78, memory: 89 }
|
||||
]
|
||||
}
|
||||
|
||||
# Insight generation functions
|
||||
def generate_infrastructure_insights [infra_data: any, metrics: record] {
|
||||
mut insights = []
|
||||
|
||||
if ($metrics.cpu > 80) {
|
||||
$insights = ($insights | append "⚠️ High CPU usage detected across infrastructure")
|
||||
}
|
||||
|
||||
if ($metrics.memory > 85) {
|
||||
$insights = ($insights | append "🚨 Memory usage is approaching critical levels")
|
||||
}
|
||||
|
||||
$insights = ($insights | append "✅ Infrastructure monitoring active and collecting data")
|
||||
$insights
|
||||
}
|
||||
|
||||
def generate_performance_insights [perf_data: any] {
|
||||
[
|
||||
"📊 Performance analysis completed"
|
||||
"🔍 Bottlenecks identified in database tier"
|
||||
"⚡ Optimization opportunities available"
|
||||
]
|
||||
}
|
||||
|
||||
def generate_cost_insights [cost_data: any] {
|
||||
[
|
||||
"💰 Cost analysis reveals optimization opportunities"
|
||||
"📉 Potential savings identified in compute resources"
|
||||
"🎯 Right-sizing recommendations available"
|
||||
]
|
||||
}
|
||||
|
||||
def generate_security_insights [security_data: any] {
|
||||
[
|
||||
"🛡️ Security posture assessment completed"
|
||||
"🔍 No critical vulnerabilities detected"
|
||||
"✅ Compliance requirements being met"
|
||||
]
|
||||
}
|
||||
|
||||
def generate_predictive_insights [prediction_data: any] {
|
||||
[
|
||||
"🔮 Predictive models trained on historical data"
|
||||
"📈 Trend analysis shows stable resource usage"
|
||||
"⏰ Early warning system active"
|
||||
]
|
||||
}
|
||||
|
||||
def generate_troubleshooting_insights [troubleshoot_data: any] {
|
||||
[
|
||||
"🔧 Issue patterns identified"
|
||||
"🎯 Root cause analysis in progress"
|
||||
"💡 Solution recommendations generated"
|
||||
]
|
||||
}
|
||||
|
||||
# Recommendation generation
|
||||
def generate_recommendations [category: string, data: any] {
|
||||
match $category {
|
||||
"infrastructure" => [
|
||||
"Consider implementing auto-scaling for peak hours"
|
||||
"Review resource allocation across services"
|
||||
"Set up additional monitoring alerts"
|
||||
]
|
||||
"performance" => [
|
||||
"Optimize database queries causing slow responses"
|
||||
"Implement caching for frequently accessed data"
|
||||
"Scale up instances experiencing high load"
|
||||
]
|
||||
"cost" => [
|
||||
"Right-size over-provisioned instances"
|
||||
"Implement scheduled shutdown for dev environments"
|
||||
"Consider reserved instances for stable workloads"
|
||||
]
|
||||
"security" => [
|
||||
"Update security patches on all systems"
|
||||
"Implement multi-factor authentication"
|
||||
"Review and rotate access credentials"
|
||||
]
|
||||
"predictive" => [
|
||||
"Plan capacity increases for projected growth"
|
||||
"Set up proactive monitoring for predicted issues"
|
||||
"Prepare scaling strategies for anticipated load"
|
||||
]
|
||||
"troubleshooting" => [
|
||||
"Implement fix for identified root cause"
|
||||
"Add monitoring to prevent recurrence"
|
||||
"Update documentation with solution steps"
|
||||
]
|
||||
_ => [
|
||||
"Continue monitoring system health"
|
||||
"Review configuration regularly"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Response formatting
|
||||
def format_response [result: record, format: string] {
|
||||
match $format {
|
||||
"json" => {
|
||||
$result | to json
|
||||
}
|
||||
"yaml" => {
|
||||
$result | to yaml
|
||||
}
|
||||
"table" => {
|
||||
$result | table
|
||||
}
|
||||
"summary" => {
|
||||
generate_summary $result
|
||||
}
|
||||
_ => {
|
||||
$result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generate_summary [result: record] {
|
||||
let insights_text = ($result.insights | str join "\n• ")
|
||||
let recs_text = ($result.recommendations | str join "\n• ")
|
||||
|
||||
$"
|
||||
🤖 AI Query Analysis Results
|
||||
|
||||
Query: ($result.query)
|
||||
Type: ($result.type)
|
||||
Timestamp: ($result.timestamp)
|
||||
|
||||
📊 Key Insights:
|
||||
• ($insights_text)
|
||||
|
||||
💡 Recommendations:
|
||||
• ($recs_text)
|
||||
|
||||
📋 Summary: Analysis completed successfully with actionable insights generated.
|
||||
"
|
||||
}
|
||||
|
||||
# Batch query processing
|
||||
export def process_batch_queries [
|
||||
queries: list<string>
|
||||
--context: string = "batch"
|
||||
--format: string = "json"
|
||||
--parallel = true
|
||||
] {
|
||||
|
||||
print $"🔄 Processing batch of ($queries | length) queries..."
|
||||
|
||||
if $parallel {
|
||||
$queries | par-each {|query|
|
||||
process_query $query --context $context --format $format
|
||||
}
|
||||
} else {
|
||||
$queries | each {|query|
|
||||
process_query $query --context $context --format $format
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Query performance analytics
|
||||
export def analyze_query_performance [
|
||||
queries: list<string>
|
||||
--iterations: int = 10
|
||||
] {
|
||||
|
||||
print "📊 Analyzing query performance..."
|
||||
|
||||
mut results = []
|
||||
|
||||
for query in $queries {
|
||||
let start_time = (date now)
|
||||
let _ = (process_query $query --format "json")
|
||||
let end_time = (date now)
|
||||
let duration = ($end_time - $start_time)
|
||||
|
||||
$results = ($results | append {
|
||||
query: $query
|
||||
duration_ms: ($duration | into int)
|
||||
timestamp: $start_time
|
||||
})
|
||||
}
|
||||
|
||||
let avg_duration = ($results | get duration_ms | math avg)
|
||||
let total_queries = ($results | length)
|
||||
|
||||
{
|
||||
total_queries: $total_queries
|
||||
average_duration_ms: $avg_duration
|
||||
queries_per_second: (1000 / $avg_duration)
|
||||
results: $results
|
||||
analysis: {
|
||||
fastest_query: ($results | sort-by duration_ms | first)
|
||||
slowest_query: ($results | sort-by duration_ms | last)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Export query capabilities
|
||||
export def get_query_capabilities [] {
|
||||
{
|
||||
supported_types: $QUERY_TYPES
|
||||
agents: [
|
||||
"pattern_detector"
|
||||
"cost_optimizer"
|
||||
"performance_analyzer"
|
||||
"security_monitor"
|
||||
"predictor"
|
||||
"auto_healer"
|
||||
]
|
||||
output_formats: ["json", "yaml", "table", "summary"]
|
||||
features: [
|
||||
"natural_language_processing"
|
||||
"entity_extraction"
|
||||
"agent_selection"
|
||||
"parallel_processing"
|
||||
"performance_analytics"
|
||||
"batch_queries"
|
||||
]
|
||||
examples: {
|
||||
infrastructure: "What servers are currently running?"
|
||||
performance: "Which services are using the most CPU?"
|
||||
cost: "How can I reduce my AWS costs?"
|
||||
security: "Are there any security threats detected?"
|
||||
predictive: "When will I need to scale my database?"
|
||||
troubleshooting: "Why is the web service responding slowly?"
|
||||
}
|
||||
}
|
||||
}
|
||||
64
nulib/catalog/component_scripts.nu
Normal file
64
nulib/catalog/component_scripts.nu
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use tools/nickel/process.nu [ncl-eval, default-ncl-paths]
|
||||
|
||||
def disc-infra-name [ws_root: string]: nothing -> string {
|
||||
let infra_dir = ($ws_root | path join "infra")
|
||||
if not ($infra_dir | path exists) {
|
||||
error make {msg: $"No infra/ directory in workspace ($ws_root)"}
|
||||
}
|
||||
let candidates = (
|
||||
ls $infra_dir
|
||||
| where type == dir
|
||||
| get name
|
||||
| each {|p| $p | path basename }
|
||||
| where {|n| ($ws_root | path join "infra" $n "settings.ncl") | path exists }
|
||||
)
|
||||
if ($candidates | is-empty) {
|
||||
error make {msg: $"No infra/<name>/settings.ncl found under ($ws_root)/infra/"}
|
||||
}
|
||||
$candidates | first
|
||||
}
|
||||
|
||||
# Resolve the Tier-2 (op-specific) or Tier-1 (CMD_TASK dispatch) script path
|
||||
# for a component operation. Returns {path: string, tier: int}.
|
||||
#
|
||||
# Tier 2: catalog/components/<name>/<mode>/<op>-<name>.sh (preferred — per-op file)
|
||||
# Tier 1: catalog/components/<name>/<mode>/install-<name>.sh (CMD_TASK dispatch, pass op as $1)
|
||||
export def get-component-script-path [
|
||||
name: string
|
||||
mode: string
|
||||
op: string
|
||||
] {
|
||||
let prov = ($env.PROVISIONING? | default "")
|
||||
if ($prov | is-empty) {
|
||||
error make {msg: "PROVISIONING env var not set — cannot resolve component script path"}
|
||||
}
|
||||
let base = ($prov | path join "catalog" "components" $name $mode)
|
||||
let tier2 = ($base | path join $"($op)-($name).sh")
|
||||
if ($tier2 | path exists) { return {path: $tier2, tier: 2} }
|
||||
let tier1 = ($base | path join $"install-($name).sh")
|
||||
if ($tier1 | path exists) { return {path: $tier1, tier: 1} }
|
||||
error make {msg: $"No script for '($name)' mode=($mode) op=($op)\n Tier2 tried: ($tier2)\n Tier1 tried: ($tier1)"}
|
||||
}
|
||||
|
||||
# Validate that an operation is enabled for a component in the workspace.
|
||||
# Reads ComponentDef.operations from the workspace NCL. Defaults to enabled=true
|
||||
# if the operations record does not contain the op key.
|
||||
export def validate-component-op [
|
||||
ws_root: string
|
||||
component: string
|
||||
op: string
|
||||
]: nothing -> nothing {
|
||||
let infra = (disc-infra-name $ws_root)
|
||||
let comp_ncl = ($ws_root | path join "infra" $infra "components" $"($component).ncl")
|
||||
if not ($comp_ncl | path exists) {
|
||||
error make {msg: $"Component '($component)' not in workspace ($ws_root)"}
|
||||
}
|
||||
let exported = (ncl-eval $comp_ncl (default-ncl-paths $ws_root))
|
||||
let inner = ($exported | get -o $component | default null)
|
||||
if ($inner == null) { return }
|
||||
let ops = ($inner | get -o operations | default {})
|
||||
let enabled = if ($op in $ops) { $ops | get $op } else { true }
|
||||
if not $enabled {
|
||||
error make {msg: $"Operation '($op)' is disabled for component '($component)' — set operations.($op) = true in workspace to enable"}
|
||||
}
|
||||
}
|
||||
155
nulib/catalog/runner.nu
Normal file
155
nulib/catalog/runner.nu
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# infra-catalog Runner Interface — ComputeProvider contract
|
||||
#
|
||||
# Defines the contract that any compute-provider component must satisfy.
|
||||
# A conforming implementation is a script (typically runner.nu in the component
|
||||
# root) that accepts subcommands with JSON on stdin and writes JSON to stdout.
|
||||
#
|
||||
# Invocation pattern (callers always go through `runner-dispatch`):
|
||||
# {cpu: 4, memory_gb: 8, ...} | runner-dispatch "/path/to/provider/runner.nu" spawn
|
||||
#
|
||||
# Implementors source this module to validate their own I/O:
|
||||
# use catalog/runner.nu [validate-spawn-input, validate-spawn-output]
|
||||
#
|
||||
# The interface is intentionally narrow: lian-build, Vapora, and any other tool
|
||||
# that needs ephemeral compute only need these four commands. Provider-specific
|
||||
# operations (snapshot management, network topology, etc.) belong in the
|
||||
# provider's own extended interface, not here.
|
||||
|
||||
# ── Input / output shapes ─────────────────────────────────────────────────────
|
||||
|
||||
# Shape of the JSON record passed to `spawn`.
|
||||
# All fields that carry provider-specific meaning are string references:
|
||||
# the caller does not interpret them — the provider script does.
|
||||
export def spawn-input-shape []: nothing -> record {
|
||||
{
|
||||
cpu: { type: "int", required: true, doc: "Minimum CPU cores." }
|
||||
memory_gb: { type: "int", required: true, doc: "Minimum RAM in GB." }
|
||||
disk_gb: { type: "int", required: true, doc: "Minimum root disk in GB." }
|
||||
time_budget_min: { type: "int", required: true, doc: "Expected lifetime. Provider may enforce TTL." }
|
||||
image_ref: { type: "string", required: true, doc: "Provider-specific image/snapshot reference." }
|
||||
location: { type: "string", required: true, doc: "Provider-specific region or datacenter." }
|
||||
ssh_key_ref: { type: "string", required: true, doc: "SSH key name registered with the provider." }
|
||||
network_ref: { type: "string", required: false, doc: "Private network name or null." }
|
||||
firewall_ref: { type: "string", required: false, doc: "Firewall name or null." }
|
||||
tags: { type: "record", required: false, doc: "Arbitrary key-value labels." }
|
||||
}
|
||||
}
|
||||
|
||||
# Shape of the JSON record returned by `spawn`.
|
||||
# `handle` is opaque to the caller — it is stored and returned verbatim
|
||||
# to `destroy` and `status`. Providers encode whatever they need in it
|
||||
# (server name, instance ID, etc.).
|
||||
export def spawn-output-shape []: nothing -> record {
|
||||
{
|
||||
handle: { type: "string", required: true, doc: "Opaque provider reference. Returned to destroy/status." }
|
||||
ssh_host: { type: "string", required: true, doc: "IP or hostname to reach the runner." }
|
||||
ssh_port: { type: "int", required: true, doc: "SSH port." }
|
||||
expires_at: { type: "string", required: false, doc: "ISO-8601 expiry, or null if provider has no TTL." }
|
||||
}
|
||||
}
|
||||
|
||||
# Shape passed to `destroy` and `status`.
|
||||
export def handle-input-shape []: nothing -> record {
|
||||
{
|
||||
handle: { type: "string", required: true, doc: "Value returned by spawn." }
|
||||
}
|
||||
}
|
||||
|
||||
# Shape returned by `status`.
|
||||
export def status-output-shape []: nothing -> record {
|
||||
{
|
||||
running: { type: "bool", required: true, doc: "Whether the runner is reachable." }
|
||||
ssh_host: { type: "string", required: false, doc: "Current IP, if available." }
|
||||
}
|
||||
}
|
||||
|
||||
# Shape returned by `describe`.
|
||||
export def describe-output-shape []: nothing -> record {
|
||||
{
|
||||
locations: { type: "list<string>", required: true, doc: "Available regions/datacenters." }
|
||||
server_types: {
|
||||
type: "list<record>",
|
||||
required: true,
|
||||
doc: "Available sizes. Each record: {name, cpu, memory_gb, disk_gb}."
|
||||
}
|
||||
image_query: { type: "string", required: false, doc: "How to discover available images, or null." }
|
||||
}
|
||||
}
|
||||
|
||||
# ── Validators ────────────────────────────────────────────────────────────────
|
||||
|
||||
# Validate a spawn input record against the contract.
|
||||
# Errors immediately with a descriptive message on contract violation.
|
||||
export def validate-spawn-input []: record -> record {
|
||||
let input = $in
|
||||
let required = [cpu, memory_gb, disk_gb, time_budget_min, image_ref, location, ssh_key_ref]
|
||||
let missing = $required | where { |f| not ($f in ($input | columns)) }
|
||||
if not ($missing | is-empty) {
|
||||
error make { msg: $"runner spawn: missing required fields: ($missing | str join ', ')" }
|
||||
}
|
||||
if ($input.cpu | into int) < 1 {
|
||||
error make { msg: "runner spawn: cpu must be >= 1" }
|
||||
}
|
||||
if ($input.memory_gb | into int) < 1 {
|
||||
error make { msg: "runner spawn: memory_gb must be >= 1" }
|
||||
}
|
||||
$input
|
||||
}
|
||||
|
||||
# Validate a spawn output record before returning it to the caller.
|
||||
export def validate-spawn-output []: record -> record {
|
||||
let output = $in
|
||||
let required = [handle, ssh_host, ssh_port]
|
||||
let missing = $required | where { |f| not ($f in ($output | columns)) }
|
||||
if not ($missing | is-empty) {
|
||||
error make { msg: $"runner spawn output: missing required fields: ($missing | str join ', ')" }
|
||||
}
|
||||
$output
|
||||
}
|
||||
|
||||
# ── Dispatcher ───────────────────────────────────────────────────────────────
|
||||
#
|
||||
# The dispatcher is the single point through which all tools invoke a provider.
|
||||
# It isolates callers from the path and invocation mechanics of the provider
|
||||
# script. Adding or replacing a provider never requires changes in the caller.
|
||||
#
|
||||
# Usage:
|
||||
# {cpu: 4, memory_gb: 8, disk_gb: 20, time_budget_min: 60,
|
||||
# image_ref: "buildkit-runner-latest", location: "fsn1",
|
||||
# ssh_key_ref: "orchestrator-buildkit"}
|
||||
# | runner-dispatch "/catalog/providers/hetzner/runner.nu" spawn
|
||||
#
|
||||
# {handle: "buildkit-runner-abc123"} | runner-dispatch "/catalog/providers/hetzner/runner.nu" destroy
|
||||
|
||||
export def runner-dispatch [
|
||||
script: path, # absolute path to the provider's runner.nu
|
||||
command: string, # spawn | destroy | status | describe
|
||||
]: record -> any {
|
||||
let input = $in
|
||||
|
||||
if not ($script | path exists) {
|
||||
error make { msg: $"runner-dispatch: provider script not found: ($script)" }
|
||||
}
|
||||
|
||||
let allowed = [spawn destroy status describe]
|
||||
if not ($command in $allowed) {
|
||||
error make { msg: $"runner-dispatch: unknown command '($command)'. Allowed: ($allowed | str join ', ')" }
|
||||
}
|
||||
|
||||
let json_input = $input | to json
|
||||
let result = $json_input | nu $script $command | complete
|
||||
|
||||
if $result.exit_code != 0 {
|
||||
error make {
|
||||
msg: $"runner ($command) failed (exit ($result.exit_code))"
|
||||
label: {
|
||||
text: ($result.stderr | str trim)
|
||||
span: (metadata $script).span
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if $command in [spawn status describe] {
|
||||
$result.stdout | str trim | from json
|
||||
}
|
||||
}
|
||||
431
nulib/cli/ai.nu
Normal file
431
nulib/cli/ai.nu
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
# AI Module for Provisioning CLI
|
||||
# Enhanced natural language interface with intelligent agents
|
||||
|
||||
use std
|
||||
use platform/ai/lib.nu *
|
||||
use platform/settings/loader.nu load_settings
|
||||
use platform/plugins_defs.nu render_template
|
||||
use ai/query_processor.nu *
|
||||
|
||||
# Main AI command dispatcher
|
||||
export def main [
|
||||
action: string
|
||||
...args: string
|
||||
--prompt: string
|
||||
--template-type: string = "server"
|
||||
--context: string
|
||||
--provider: string
|
||||
--model: string
|
||||
--max-tokens: int
|
||||
--temperature: float
|
||||
--test
|
||||
--config
|
||||
--enable
|
||||
--disable
|
||||
] {
|
||||
match $action {
|
||||
"template" => { ai_template_command $args $prompt $template_type }
|
||||
"query" => {
|
||||
if ($prompt | is-not-empty) {
|
||||
enhanced_query_command $prompt $context
|
||||
} else {
|
||||
ai_query_command $args $prompt $context
|
||||
}
|
||||
}
|
||||
"chat" => { start_interactive_chat }
|
||||
"capabilities" => { show_ai_capabilities }
|
||||
"examples" => { show_query_examples }
|
||||
"batch" => {
|
||||
if ($args | length) > 0 {
|
||||
process_batch_file $args.0
|
||||
} else {
|
||||
print "❌ Batch processing requires a file path"
|
||||
}
|
||||
}
|
||||
"performance" => { run_ai_benchmark }
|
||||
"webhook" => { ai_webhook_command $args $prompt }
|
||||
"test" => { ai_test_command }
|
||||
"config" => { ai_config_command }
|
||||
"enable" => { ai_enable_command }
|
||||
"disable" => { ai_disable_command }
|
||||
"help" => { enhanced_ai_help_command }
|
||||
_ => {
|
||||
print $"Unknown AI action: ($action)"
|
||||
enhanced_ai_help_command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Generate infrastructure templates using AI
|
||||
def ai_template_command [
|
||||
args: list<string>
|
||||
prompt: string
|
||||
template_type: string
|
||||
] {
|
||||
if ($prompt | is-empty) {
|
||||
error make {msg: "AI template generation requires --prompt"}
|
||||
}
|
||||
|
||||
let result = (ai_generate_template $prompt $template_type)
|
||||
print $"# AI Generated ($template_type) Template"
|
||||
print $"# Prompt: ($prompt)"
|
||||
print ""
|
||||
print $result
|
||||
}
|
||||
|
||||
# Process natural language queries about infrastructure
|
||||
def ai_query_command [
|
||||
args: list<string>
|
||||
prompt: string
|
||||
context: string
|
||||
] {
|
||||
if ($prompt | is-empty) {
|
||||
error make {msg: "AI query requires --prompt"}
|
||||
}
|
||||
|
||||
let context_data = if ($context | is-empty) {
|
||||
{}
|
||||
} else {
|
||||
if ($context | str starts-with "{") {
|
||||
($context | from json)
|
||||
} else {
|
||||
{raw_context: $context}
|
||||
}
|
||||
}
|
||||
|
||||
let result = (ai_process_query $prompt $context_data)
|
||||
print $result
|
||||
}
|
||||
|
||||
# Process webhook/chat messages
|
||||
def ai_webhook_command [
|
||||
args: list<string>
|
||||
prompt: string
|
||||
] {
|
||||
if ($prompt | is-empty) {
|
||||
error make {msg: "AI webhook processing requires --prompt"}
|
||||
}
|
||||
|
||||
let user_id = if ($args | length) > 0 { $args.0 } else { "cli" }
|
||||
let channel = if ($args | length) > 1 { $args.1 } else { "direct" }
|
||||
|
||||
let result = (ai_process_webhook $prompt $user_id $channel)
|
||||
print $result
|
||||
}
|
||||
|
||||
# Test AI connectivity and configuration
|
||||
def ai_test_command [] {
|
||||
print "Testing AI configuration..."
|
||||
|
||||
let validation = (validate_ai_config)
|
||||
if not $validation.valid {
|
||||
print "❌ AI configuration issues found:"
|
||||
for issue in $validation.issues {
|
||||
print $" - ($issue)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
print "✅ AI configuration is valid"
|
||||
|
||||
let test_result = (test_ai_connection)
|
||||
if $test_result.success {
|
||||
print $"✅ ($test_result.message)"
|
||||
if "response" in $test_result {
|
||||
print $" Response: ($test_result.response)"
|
||||
}
|
||||
} else {
|
||||
print $"❌ ($test_result.message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Show AI configuration
|
||||
def ai_config_command [] {
|
||||
let config = (get_ai_config)
|
||||
|
||||
print "🤖 AI Configuration:"
|
||||
print $" Enabled: ($config.enabled)"
|
||||
print $" Provider: ($config.provider)"
|
||||
print $" Model: ($config.model? // 'default')"
|
||||
print $" Max Tokens: ($config.max_tokens)"
|
||||
print $" Temperature: ($config.temperature)"
|
||||
print $" Timeout: ($config.timeout)s"
|
||||
print ""
|
||||
print "Feature Flags:"
|
||||
print $" Template AI: ($config.enable_template_ai)"
|
||||
print $" Query AI: ($config.enable_query_ai)"
|
||||
print $" Webhook AI: ($config.enable_webhook_ai)"
|
||||
|
||||
if $config.enabled and ($config.api_key? == null) {
|
||||
print ""
|
||||
print "⚠️ API key not configured"
|
||||
print " Set environment variable based on provider:"
|
||||
print " - OpenAI: OPENAI_API_KEY"
|
||||
print " - Claude: ANTHROPIC_API_KEY"
|
||||
print " - Generic: LLM_API_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
# Enable AI functionality
|
||||
def ai_enable_command [] {
|
||||
print "AI functionality can be enabled by setting ai.enabled = true in your Nickel settings"
|
||||
print "Example configuration:"
|
||||
print ""
|
||||
print "ai: AIProvider {"
|
||||
print " enabled: true"
|
||||
print " provider: \"openai\" # or \"claude\" or \"generic\""
|
||||
print " api_key: env(\"OPENAI_API_KEY\")"
|
||||
print " model: \"gpt-4\""
|
||||
print " max_tokens: 2048"
|
||||
print " temperature: 0.3"
|
||||
print " enable_template_ai: true"
|
||||
print " enable_query_ai: true"
|
||||
print " enable_webhook_ai: false"
|
||||
print "}"
|
||||
}
|
||||
|
||||
# Disable AI functionality
|
||||
def ai_disable_command [] {
|
||||
print "AI functionality can be disabled by setting ai.enabled = false in your Nickel settings"
|
||||
print "This will disable all AI features while preserving configuration."
|
||||
}
|
||||
|
||||
# Show AI help
|
||||
def ai_help_command [] {
|
||||
print "🤖 AI-Powered Provisioning Commands"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " ./core/nulib/provisioning ai <ACTION> [OPTIONS]"
|
||||
print ""
|
||||
print "ACTIONS:"
|
||||
print " template Generate infrastructure templates from natural language"
|
||||
print " query Process natural language queries about infrastructure"
|
||||
print " webhook Process webhook/chat messages"
|
||||
print " test Test AI connectivity and configuration"
|
||||
print " config Show current AI configuration"
|
||||
print " enable Show how to enable AI functionality"
|
||||
print " disable Show how to disable AI functionality"
|
||||
print " help Show this help message"
|
||||
print ""
|
||||
print "TEMPLATE OPTIONS:"
|
||||
print " --prompt <text> Natural language description"
|
||||
print " --template-type <type> Type of template (server, cluster, taskserv)"
|
||||
print ""
|
||||
print "QUERY OPTIONS:"
|
||||
print " --prompt <text> Natural language query"
|
||||
print " --context <json> Additional context as JSON"
|
||||
print ""
|
||||
print "WEBHOOK OPTIONS:"
|
||||
print " --prompt <text> Message to process"
|
||||
print " <user_id> User ID for context"
|
||||
print " <channel> Channel for context"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " # Generate a Kubernetes cluster template"
|
||||
print " ./core/nulib/provisioning ai template --prompt \"3-node Kubernetes cluster with Ceph storage\""
|
||||
print ""
|
||||
print " # Query infrastructure status"
|
||||
print " ./core/nulib/provisioning ai query --prompt \"show all running servers with high CPU\""
|
||||
print ""
|
||||
print " # Process chat message"
|
||||
print " ./core/nulib/provisioning ai webhook --prompt \"deploy redis cluster\" user123 slack"
|
||||
print ""
|
||||
print " # Test AI configuration"
|
||||
print " ./core/nulib/provisioning ai test"
|
||||
}
|
||||
|
||||
# AI-enhanced generate command
|
||||
export def ai_generate [
|
||||
type: string
|
||||
--prompt: string
|
||||
--template-type: string = "server"
|
||||
--output: string
|
||||
] {
|
||||
if ($prompt | is-empty) {
|
||||
error make {msg: "AI generation requires --prompt"}
|
||||
}
|
||||
|
||||
let result = (ai_generate_template $prompt $template_type)
|
||||
|
||||
if ($output | is-empty) {
|
||||
print $result
|
||||
} else {
|
||||
$result | save $output
|
||||
print $"AI generated ($template_type) saved to: ($output)"
|
||||
}
|
||||
}
|
||||
|
||||
# AI-enhanced query with provisioning context
|
||||
export def ai_query_infra [
|
||||
query: string
|
||||
--infra: string
|
||||
--provider: string
|
||||
--output-format: string = "human"
|
||||
] {
|
||||
let context = {
|
||||
infra: ($infra | default "")
|
||||
provider: ($provider | default "")
|
||||
output_format: $output_format
|
||||
}
|
||||
|
||||
let result = (ai_process_query $query $context)
|
||||
|
||||
match $output_format {
|
||||
"json" => { {query: $query, response: $result} | to json }
|
||||
"yaml" => { {query: $query, response: $result} | to yaml }
|
||||
_ => { print $result }
|
||||
}
|
||||
}
|
||||
|
||||
# Enhanced AI query command with intelligent agents
|
||||
def enhanced_query_command [
|
||||
prompt: string
|
||||
context: string
|
||||
] {
|
||||
print $"🤖 Enhanced AI Query: ($prompt)"
|
||||
|
||||
let result = process_query $prompt --format "summary"
|
||||
print $result
|
||||
}
|
||||
|
||||
# Show AI system capabilities
|
||||
def show_ai_capabilities [] {
|
||||
let caps = get_query_capabilities
|
||||
|
||||
print "🤖 Enhanced AI System Capabilities"
|
||||
print ""
|
||||
print "📋 Supported Query Types:"
|
||||
$caps.supported_types | each { |type| print $" • ($type)" }
|
||||
|
||||
print ""
|
||||
print "🤖 Available AI Agents:"
|
||||
$caps.agents | each { |agent| print $" • ($agent)" }
|
||||
|
||||
print ""
|
||||
print "📊 Output Formats:"
|
||||
$caps.output_formats | each { |format| print $" • ($format)" }
|
||||
|
||||
print ""
|
||||
print "🚀 Features:"
|
||||
$caps.features | each { |feature| print $" • ($feature)" }
|
||||
}
|
||||
|
||||
# Show query examples
|
||||
def show_query_examples [] {
|
||||
print "💡 Enhanced AI Query Examples"
|
||||
print ""
|
||||
|
||||
print "🏗️ Infrastructure Status:"
|
||||
print " • \"What servers are currently running?\""
|
||||
print " • \"Show me the health status of all services\""
|
||||
print " • \"Which containers are consuming the most resources?\""
|
||||
print ""
|
||||
|
||||
print "⚡ Performance Analysis:"
|
||||
print " • \"Which services have high CPU usage?\""
|
||||
print " • \"What's causing slow response times?\""
|
||||
print " • \"Show me memory usage trends over the last hour\""
|
||||
print ""
|
||||
|
||||
print "💰 Cost Optimization:"
|
||||
print " • \"How can I reduce my AWS costs?\""
|
||||
print " • \"Which instances are underutilized?\""
|
||||
print " • \"Show me the most expensive resources\""
|
||||
print ""
|
||||
|
||||
print "🛡️ Security Analysis:"
|
||||
print " • \"Are there any security threats detected?\""
|
||||
print " • \"Show me recent failed login attempts\""
|
||||
print " • \"What vulnerabilities exist in the system?\""
|
||||
print ""
|
||||
|
||||
print "🔮 Predictive Analysis:"
|
||||
print " • \"When will I need to scale the database?\""
|
||||
print " • \"Predict disk space usage for next month\""
|
||||
print " • \"What failures are likely to occur soon?\""
|
||||
}
|
||||
|
||||
# Process batch queries from file
|
||||
def process_batch_file [file_path: string] {
|
||||
if not ($file_path | path exists) {
|
||||
print $"❌ File not found: ($file_path)"
|
||||
return
|
||||
}
|
||||
|
||||
let queries = (open $file_path | lines | where { |line| not ($line | is-empty) and not ($line | str starts-with "#") })
|
||||
|
||||
print $"📋 Processing ($queries | length) queries from: ($file_path)"
|
||||
|
||||
let results = process_batch_queries $queries --format "summary"
|
||||
|
||||
$results | enumerate | each { |item|
|
||||
print $"--- Query ($item.index + 1) ---"
|
||||
print $item.item
|
||||
print ""
|
||||
}
|
||||
}
|
||||
|
||||
# Run AI performance benchmark
|
||||
def run_ai_benchmark [] {
|
||||
let benchmark_queries = [
|
||||
"What's the current CPU usage?"
|
||||
"Show me error logs from the last hour"
|
||||
"Which services are consuming high memory?"
|
||||
"Are there any security alerts?"
|
||||
"Predict when we'll need more storage"
|
||||
]
|
||||
|
||||
let results = analyze_query_performance $benchmark_queries
|
||||
|
||||
print "📊 AI Query Performance Benchmark"
|
||||
print $"Total Queries: ($results.total_queries)"
|
||||
print $"Average Duration: ($results.average_duration_ms) ms"
|
||||
print $"Queries per Second: ($results.queries_per_second | math round -p 2)"
|
||||
}
|
||||
|
||||
# Enhanced AI help command
|
||||
def enhanced_ai_help_command [] {
|
||||
print "🤖 Enhanced AI-Powered Provisioning Commands"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " ./core/nulib/provisioning ai <ACTION> [OPTIONS]"
|
||||
print ""
|
||||
print "ENHANCED ACTIONS:"
|
||||
print " query Process natural language queries with intelligent agents"
|
||||
print " chat Interactive AI chat mode"
|
||||
print " capabilities Show AI system capabilities"
|
||||
print " examples Show example queries"
|
||||
print " batch Process batch queries from file"
|
||||
print " performance Run performance benchmarks"
|
||||
print ""
|
||||
print "LEGACY ACTIONS:"
|
||||
print " template Generate infrastructure templates"
|
||||
print " webhook Process webhook/chat messages"
|
||||
print " test Test AI connectivity"
|
||||
print " config Show AI configuration"
|
||||
print " enable Enable AI functionality"
|
||||
print " disable Disable AI functionality"
|
||||
print ""
|
||||
print "ENHANCED QUERY EXAMPLES:"
|
||||
print " # Natural language infrastructure queries"
|
||||
print " ./core/nulib/provisioning ai query --prompt \"What servers are using high CPU?\""
|
||||
print " ./core/nulib/provisioning ai query --prompt \"How can I reduce AWS costs?\""
|
||||
print " ./core/nulib/provisioning ai query --prompt \"Are there any security threats?\""
|
||||
print ""
|
||||
print " # Interactive chat mode"
|
||||
print " ./core/nulib/provisioning ai chat"
|
||||
print ""
|
||||
print " # Batch processing"
|
||||
print " ./core/nulib/provisioning ai batch queries.txt"
|
||||
print ""
|
||||
print " # Performance analysis"
|
||||
print " ./core/nulib/provisioning ai performance"
|
||||
print ""
|
||||
print "🚀 New Features:"
|
||||
print " • Intelligent agent selection"
|
||||
print " • Natural language processing"
|
||||
print " • Real-time data integration"
|
||||
print " • Predictive analytics"
|
||||
print " • Interactive chat mode"
|
||||
print " • Batch query processing"
|
||||
}
|
||||
347
nulib/cli/api.nu
Normal file
347
nulib/cli/api.nu
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# Hetzner Cloud HTTP API Client
|
||||
use env.nu *
|
||||
|
||||
# Get Bearer token for API authentication
|
||||
export def hetzner_api_auth []: nothing -> string {
|
||||
let token = (hetzner_api_token)
|
||||
if ($token | is-empty) {
|
||||
error make {msg: "HCLOUD_TOKEN environment variable not set. Set your Hetzner API token before using the API interface."}
|
||||
}
|
||||
$token
|
||||
}
|
||||
|
||||
# Build full API URL
|
||||
export def hetzner_api_url [path: string]: nothing -> string {
|
||||
let base = (hetzner_api_url_base)
|
||||
$"($base)($path)"
|
||||
}
|
||||
|
||||
# Generic HTTP request with error handling
|
||||
export def hetzner_api_request [method: string, path: string, data?: any]: nothing -> any {
|
||||
let token = (hetzner_api_auth)
|
||||
let url = (hetzner_api_url $path)
|
||||
|
||||
if (hetzner_debug) {
|
||||
print $"DEBUG: hetzner_api_request method=($method) path=($path) url=($url)" | encode utf8 | into string
|
||||
}
|
||||
|
||||
let headers = [Authorization $"Bearer ($token)"]
|
||||
|
||||
let result = (do {
|
||||
match $method {
|
||||
"GET" => {
|
||||
http get --headers $headers --allow-errors $url
|
||||
}
|
||||
"POST" => {
|
||||
http post --headers $headers --content-type application/json --allow-errors $url $data
|
||||
}
|
||||
"PUT" => {
|
||||
http put --headers $headers --content-type application/json --allow-errors $url $data
|
||||
}
|
||||
"DELETE" => {
|
||||
http delete --headers $headers --allow-errors $url
|
||||
}
|
||||
_ => {
|
||||
error make {msg: $"Unsupported HTTP method: ($method)"}
|
||||
}
|
||||
}
|
||||
} | complete)
|
||||
if $result.exit_code != 0 {
|
||||
error make {msg: $"Hetzner API request failed: ($result.stderr)"}
|
||||
} else {
|
||||
$result.stdout
|
||||
}
|
||||
}
|
||||
|
||||
# List all servers
|
||||
export def hetzner_api_list_servers []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/servers")
|
||||
|
||||
if ($response | describe) =~ "error" {
|
||||
error make {msg: "Failed to list servers from API"}
|
||||
}
|
||||
|
||||
if ($response | has servers) {
|
||||
$response.servers
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get server info by ID or name
|
||||
export def hetzner_api_server_info [id_or_name: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" $"/servers/($id_or_name)")
|
||||
|
||||
if ($response | describe) =~ "error" {
|
||||
error make {msg: $"Server not found: ($id_or_name)"}
|
||||
}
|
||||
|
||||
if ($response | has server) {
|
||||
$response.server
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Create a new server
|
||||
export def hetzner_api_create_server [config: record]: nothing -> record {
|
||||
if (hetzner_debug) {
|
||||
print $"DEBUG: Creating server with config: ($config | to json)" | encode utf8 | into string
|
||||
}
|
||||
|
||||
let response = (hetzner_api_request "POST" "/servers" $config)
|
||||
|
||||
if ($response | describe) =~ "error" {
|
||||
error make {msg: $"Failed to create server: ($response)"}
|
||||
}
|
||||
|
||||
if ($response | has server) {
|
||||
$response.server
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Delete a server
|
||||
export def hetzner_api_delete_server [id: string]: nothing -> nothing {
|
||||
let response = (hetzner_api_request "DELETE" $"/servers/($id)")
|
||||
null
|
||||
}
|
||||
|
||||
# Perform server action (start, stop, reboot, etc.)
|
||||
export def hetzner_api_server_action [id: string, action: string]: nothing -> record {
|
||||
let data = {action: $action}
|
||||
let response = (hetzner_api_request "POST" $"/servers/($id)/actions/($action)" $data)
|
||||
|
||||
if ($response | has action) {
|
||||
$response.action
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List all locations
|
||||
export def hetzner_api_list_locations []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/locations")
|
||||
|
||||
if ($response | has locations) {
|
||||
$response.locations
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# List all server types
|
||||
export def hetzner_api_list_server_types []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/server_types")
|
||||
|
||||
if ($response | has server_types) {
|
||||
$response.server_types
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get server type info
|
||||
export def hetzner_api_server_type_info [id_or_name: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" $"/server_types/($id_or_name)")
|
||||
|
||||
if ($response | has server_type) {
|
||||
$response.server_type
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List all images
|
||||
export def hetzner_api_list_images []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/images")
|
||||
|
||||
if ($response | has images) {
|
||||
$response.images
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# List all volumes
|
||||
export def hetzner_api_list_volumes []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/volumes")
|
||||
|
||||
if ($response | has volumes) {
|
||||
$response.volumes
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Create a volume
|
||||
export def hetzner_api_create_volume [config: record]: nothing -> record {
|
||||
let response = (hetzner_api_request "POST" "/volumes" $config)
|
||||
|
||||
if ($response | has volume) {
|
||||
$response.volume
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Delete a volume
|
||||
export def hetzner_api_delete_volume [id: string]: nothing -> nothing {
|
||||
hetzner_api_request "DELETE" $"/volumes/($id)"
|
||||
null
|
||||
}
|
||||
|
||||
# Attach volume to server
|
||||
export def hetzner_api_attach_volume [volume_id: string, server_id: string]: nothing -> record {
|
||||
let data = {
|
||||
server: ($server_id | into int)
|
||||
automount: false
|
||||
}
|
||||
let response = (hetzner_api_request "POST" $"/volumes/($volume_id)/actions/attach" $data)
|
||||
|
||||
if ($response | has action) {
|
||||
$response.action
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Detach volume from server
|
||||
export def hetzner_api_detach_volume [volume_id: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "POST" $"/volumes/($volume_id)/actions/detach" {})
|
||||
|
||||
if ($response | has action) {
|
||||
$response.action
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List all networks
|
||||
export def hetzner_api_list_networks []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/networks")
|
||||
|
||||
if ($response | has networks) {
|
||||
$response.networks
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get network info
|
||||
export def hetzner_api_network_info [id_or_name: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" $"/networks/($id_or_name)")
|
||||
|
||||
if ($response | has network) {
|
||||
$response.network
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Attach network to server
|
||||
export def hetzner_api_attach_network [server_id: string, network_id: string, ip?: string]: nothing -> record {
|
||||
let data = if ($ip != null) {
|
||||
{server: ($server_id | into int), network: ($network_id | into int), ip: $ip}
|
||||
} else {
|
||||
{server: ($server_id | into int), network: ($network_id | into int)}
|
||||
}
|
||||
|
||||
let response = (hetzner_api_request "POST" $"/servers/($server_id)/actions/attach_to_network" $data)
|
||||
|
||||
if ($response | has action) {
|
||||
$response.action
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Detach network from server
|
||||
export def hetzner_api_detach_network [server_id: string, network_id: string]: nothing -> record {
|
||||
let data = {network: ($network_id | into int)}
|
||||
let response = (hetzner_api_request "POST" $"/servers/($server_id)/actions/detach_from_network" $data)
|
||||
|
||||
if ($response | has action) {
|
||||
$response.action
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List all floating IPs
|
||||
export def hetzner_api_list_floating_ips []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/floating_ips")
|
||||
|
||||
if ($response | has floating_ips) {
|
||||
$response.floating_ips
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get pricing information
|
||||
export def hetzner_api_get_pricing []: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" "/pricing")
|
||||
|
||||
if ($response | has pricing) {
|
||||
$response.pricing
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List SSH keys
|
||||
export def hetzner_api_list_ssh_keys []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/ssh_keys")
|
||||
|
||||
if ($response | has ssh_keys) {
|
||||
$response.ssh_keys
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get SSH key info
|
||||
export def hetzner_api_ssh_key_info [id_or_name: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" $"/ssh_keys/($id_or_name)")
|
||||
|
||||
if ($response | has ssh_key) {
|
||||
$response.ssh_key
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# List firewalls
|
||||
export def hetzner_api_list_firewalls []: nothing -> list {
|
||||
let response = (hetzner_api_request "GET" "/firewalls")
|
||||
|
||||
if ($response | has firewalls) {
|
||||
$response.firewalls
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Get firewall info
|
||||
export def hetzner_api_firewall_info [id_or_name: string]: nothing -> record {
|
||||
let response = (hetzner_api_request "GET" $"/firewalls/($id_or_name)")
|
||||
|
||||
if ($response | has firewall) {
|
||||
$response.firewall
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
|
||||
# Create firewall
|
||||
export def hetzner_api_create_firewall [config: record]: nothing -> record {
|
||||
let response = (hetzner_api_request "POST" "/firewalls" $config)
|
||||
|
||||
if ($response | has firewall) {
|
||||
$response.firewall
|
||||
} else {
|
||||
$response
|
||||
}
|
||||
}
|
||||
48
nulib/cli/auth_prov.nu
Normal file
48
nulib/cli/auth_prov.nu
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for auth | login commands.
|
||||
# Loads only commands/authentication.nu.
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use cli/handlers/authentication.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--out: string = ""
|
||||
--debug (-x)
|
||||
--yes (-y)
|
||||
--check (-c)
|
||||
--notitles
|
||||
--verbose
|
||||
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let cmd = ($args | get 0? | default "")
|
||||
let ops = ($args | skip 1 | str join " ")
|
||||
let flags = (parse_common_flags {
|
||||
debug: $debug, out: ($out | default ""), notitles: $notitles,
|
||||
infra: ($infra | default ""), yes: $yes, check: $check, verbose: $verbose
|
||||
})
|
||||
handle_authentication_command $cmd $ops $flags
|
||||
}
|
||||
231
nulib/cli/backup.nu
Normal file
231
nulib/cli/backup.nu
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for backup commands. Dispatches to the prvng-backup binary
|
||||
# (the Rust backup-manager crate) for all heavy lifting; this script handles
|
||||
# argument parsing, help text, and the `policy validate` shortcut against
|
||||
# workspace declarations.
|
||||
#
|
||||
# See: ADR-010 (backup-architecture), ADR-015 (config-driven via platform-config)
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
# Resolve the prvng-backup binary. Searches:
|
||||
# 1. $PRVNG_BACKUP_BIN environment override
|
||||
# 2. $PROVISIONING/platform/target/release/prvng-backup
|
||||
# 3. $PROVISIONING/platform/target/debug/prvng-backup
|
||||
# 4. PATH lookup (`which prvng-backup`)
|
||||
# Returns "" when not found; callers handle the absence.
|
||||
def resolve-backup-bin [] {
|
||||
let override = ($env.PRVNG_BACKUP_BIN? | default "")
|
||||
if ($override | is-not-empty) {
|
||||
return $override
|
||||
}
|
||||
|
||||
let prov = ($env.PROVISIONING? | default "")
|
||||
if ($prov | is-not-empty) {
|
||||
let release = ($prov | path join "platform" "target" "release" "prvng-backup")
|
||||
if ($release | path exists) { return $release }
|
||||
|
||||
let debug = ($prov | path join "platform" "target" "debug" "prvng-backup")
|
||||
if ($debug | path exists) { return $debug }
|
||||
}
|
||||
|
||||
let lookup = (which prvng-backup | get path)
|
||||
if ($lookup | is-not-empty) {
|
||||
return ($lookup | first | into string)
|
||||
}
|
||||
|
||||
""
|
||||
}
|
||||
|
||||
# Default Nickel import path. Workspace components import from
|
||||
# 'schemas/lib/...', so the parent of the schemas/ directory is required.
|
||||
def resolve-import-path [] {
|
||||
let override = ($env.NICKEL_IMPORT_PATH? | default "")
|
||||
if ($override | is-not-empty) {
|
||||
return $override
|
||||
}
|
||||
|
||||
let prov = ($env.PROVISIONING? | default "")
|
||||
if ($prov | is-not-empty) {
|
||||
let candidate = ($prov | path expand)
|
||||
if (($candidate | path join "schemas") | path exists) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
"provisioning"
|
||||
}
|
||||
|
||||
def print-help []: nothing -> nothing {
|
||||
print "Backup Manager — multi-context backup orchestrator"
|
||||
print "==================================================="
|
||||
print ""
|
||||
print "Usage: prvng backup <subcommand> [args]"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " status Print daemon and policy status (JSON or text)"
|
||||
print " list <target> [--scope s] [--tag k=v ...]"
|
||||
print " List snapshots for a component, group, or system target"
|
||||
print " run <target> [--scope s] Trigger a one-shot backup outside cron"
|
||||
print " restore <component> Restore a snapshot (use --snapshot, --to)"
|
||||
print " restore --group <name> Restore a BackupGroup at a timestamp (use --at, --to)"
|
||||
print " mount <component> Mount a snapshot via FUSE (workstation/standalone)"
|
||||
print " verify <target> [--level lvl] Verify snapshots (quick|deep|restore-drill|full-dr)"
|
||||
print " policy validate <path> Validate a Nickel policy file or directory"
|
||||
print " policy render <path> Render a Nickel policy file as JSON"
|
||||
print " providers List available BackupProvider definitions"
|
||||
print " destinations [--check] List destinations; --check probes health"
|
||||
print " daemon <action> status|reload|drain|start"
|
||||
print " drill run <component> Invoke a verify-recipe drill on demand"
|
||||
print " events tail [--subject s] Tail NATS events for backup.>"
|
||||
print " help Show this message"
|
||||
print ""
|
||||
print "Environment:"
|
||||
print " PRVNG_BACKUP_BIN Override path to the prvng-backup binary"
|
||||
print " NICKEL_IMPORT_PATH Override Nickel --import-path (default: \$PROVISIONING)"
|
||||
print " BACKUP_CONFIG Path to backup-manager.ncl config file"
|
||||
print " BACKUP_LOG Tracing filter (default: 'info')"
|
||||
print ""
|
||||
print "Reference: ADR-010 (architecture), ADR-015 (config-driven), ADR-016 (events),"
|
||||
print " ADR-017 (vault custody)."
|
||||
}
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--workspace (-w): string = ""
|
||||
--debug (-x)
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
|
||||
let rest = if (($args | length) > 0) and (($args | first) in ["backup", "bak"]) {
|
||||
$args | skip 1
|
||||
} else {
|
||||
$args
|
||||
}
|
||||
|
||||
let sub = if (($rest | length) > 0) { $rest | first } else { "help" }
|
||||
let sub_args = if (($rest | length) > 1) { $rest | skip 1 } else { [] }
|
||||
|
||||
let bin = (resolve-backup-bin)
|
||||
if ($bin | is-empty) {
|
||||
print "ERROR: prvng-backup binary not found. Set PRVNG_BACKUP_BIN, build with"
|
||||
print " 'cargo build -p backup-manager', or add it to PATH."
|
||||
exit 1
|
||||
}
|
||||
let import_path = (resolve-import-path)
|
||||
|
||||
match $sub {
|
||||
"help" | "-h" | "--help" | "h" => { print-help }
|
||||
|
||||
"policy" => {
|
||||
let action = if (($sub_args | length) > 0) { $sub_args | first } else { "help" }
|
||||
let action_args = if (($sub_args | length) > 1) { $sub_args | skip 1 } else { [] }
|
||||
|
||||
match $action {
|
||||
"validate" => {
|
||||
let paths = if (($action_args | length) > 0) {
|
||||
$action_args
|
||||
} else {
|
||||
print "Usage: prvng backup policy validate <path> [<path> ...]"
|
||||
exit 2
|
||||
}
|
||||
let cmd_args = (["policy", "validate"] | append $paths | append ["--import-path", $import_path])
|
||||
^$bin ...$cmd_args
|
||||
}
|
||||
"render" => {
|
||||
let target = if (($action_args | length) > 0) { $action_args | first } else { "" }
|
||||
if ($target | is-empty) {
|
||||
print "Usage: prvng backup policy render <path>"
|
||||
exit 2
|
||||
}
|
||||
^$bin policy render $target --import-path $import_path
|
||||
}
|
||||
_ => {
|
||||
print "Usage: prvng backup policy <validate|render> ..."
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"status" => { ^$bin daemon status ...$sub_args }
|
||||
"list" => { ^$bin one-shot list ...$sub_args }
|
||||
"run" => { ^$bin one-shot backup ...$sub_args }
|
||||
"verify" => { ^$bin one-shot verify ...$sub_args }
|
||||
"restore" => { ^$bin one-shot restore ...$sub_args }
|
||||
|
||||
"mount" => { ^$bin standalone mount ...$sub_args }
|
||||
|
||||
"providers" => {
|
||||
print "Configured providers (from BackupProvider definitions):"
|
||||
print " restic primary; mount + streaming + encryption"
|
||||
print " kopia opt-in; global dedup, mount less battle-tested"
|
||||
print ""
|
||||
print "See provisioning/catalog/providers/backup/ for definitions."
|
||||
}
|
||||
"destinations" => {
|
||||
# Workspace declarations live in lib/backup_policies.ncl. The binary
|
||||
# will read them in a later iteration; for now we render the file.
|
||||
let prov = ($env.PROVISIONING? | default "")
|
||||
let workspace_root = ($env.WORKSPACE_ROOT? | default "")
|
||||
let candidate = if ($workspace_root | is-not-empty) {
|
||||
$workspace_root | path join "infra" "lib" "backup_policies.ncl"
|
||||
} else {
|
||||
"infra/lib/backup_policies.ncl"
|
||||
}
|
||||
if ($candidate | path exists) {
|
||||
^$bin policy render $candidate --import-path $import_path
|
||||
} else {
|
||||
print $"Workspace destinations file not found: ($candidate)"
|
||||
print "Set WORKSPACE_ROOT or run from the workspace directory."
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
|
||||
"daemon" => {
|
||||
let action = if (($sub_args | length) > 0) { $sub_args | first } else { "status" }
|
||||
let action_args = if (($sub_args | length) > 1) { $sub_args | skip 1 } else { [] }
|
||||
^$bin daemon $action ...$action_args
|
||||
}
|
||||
|
||||
"drill" => {
|
||||
let action = if (($sub_args | length) > 0) { $sub_args | first } else { "" }
|
||||
if $action != "run" {
|
||||
print "Usage: prvng backup drill run <component>"
|
||||
exit 2
|
||||
}
|
||||
let target = if (($sub_args | length) > 1) { $sub_args | get 1 } else { "" }
|
||||
if ($target | is-empty) {
|
||||
print "Usage: prvng backup drill run <component>"
|
||||
exit 2
|
||||
}
|
||||
print $"Drill mode for ($target) is wired in the CLI surface;"
|
||||
print "the daemon coordinator implementation lands in a subsequent commit."
|
||||
exit 2
|
||||
}
|
||||
|
||||
"events" => {
|
||||
let action = if (($sub_args | length) > 0) { $sub_args | first } else { "tail" }
|
||||
print $"NATS events ($action) wired in the CLI surface;"
|
||||
print "the platform-nats integration lands in a subsequent commit."
|
||||
exit 2
|
||||
}
|
||||
|
||||
_ => {
|
||||
print $"Unknown backup subcommand: ($sub)"
|
||||
print "Run: prvng backup help"
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
}
|
||||
165
nulib/cli/batch.nu
Normal file
165
nulib/cli/batch.nu
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for batch workflow commands.
|
||||
# Loads ONLY workflows/batch.nu (~95ms vs ~12s for the full double-load).
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
use orchestration/workflows/batch.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--status: string = ""
|
||||
--environment: string = ""
|
||||
--name: string = ""
|
||||
--limit: int = 50
|
||||
--format: string = "table"
|
||||
--priority: int = 5
|
||||
--interval: duration = 3sec
|
||||
--timeout: duration = 30min
|
||||
--checkpoint: string = ""
|
||||
--reason: string = ""
|
||||
--period: string = "24h"
|
||||
--from-file: string = ""
|
||||
--description: string = ""
|
||||
--check-syntax (-s)
|
||||
--check-dependencies (-d)
|
||||
--wait (-w)
|
||||
--force (-f)
|
||||
--quiet (-q)
|
||||
--detailed
|
||||
--debug (-x)
|
||||
--out: string
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
|
||||
# CMD_ARGS from the bash wrapper includes the command name as arg[0] ("batch"/"bat").
|
||||
# Strip it so arg[0] becomes the subcommand.
|
||||
let first = ($args | get 0? | default "")
|
||||
let sub_args = if $first in ["batch", "bat"] { $args | skip 1 } else { $args }
|
||||
|
||||
let task = ($sub_args | get 0? | default "")
|
||||
let ops = ($sub_args | skip 1)
|
||||
let workflow_param = ($ops | get 0? | default "")
|
||||
|
||||
match $task {
|
||||
"list" => {
|
||||
let result = (batch list --status $status --environment $environment --name $name --limit $limit --format $format)
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) } else { print ($result | table) }
|
||||
}
|
||||
"status" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow ID required"; exit 1 }
|
||||
batch status $workflow_param --format $format
|
||||
}
|
||||
"submit" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow file path required"; exit 1 }
|
||||
let result = if $wait {
|
||||
batch submit $workflow_param --name $name --priority $priority --environment $environment --wait --timeout $timeout
|
||||
} else {
|
||||
batch submit $workflow_param --name $name --priority $priority --environment $environment
|
||||
}
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) }
|
||||
}
|
||||
"validate" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow file path required"; exit 1 }
|
||||
let result = if $check_syntax and $check_dependencies {
|
||||
batch validate $workflow_param --check-syntax --check-dependencies
|
||||
} else if $check_syntax {
|
||||
batch validate $workflow_param --check-syntax
|
||||
} else if $check_dependencies {
|
||||
batch validate $workflow_param --check-dependencies
|
||||
} else {
|
||||
batch validate $workflow_param
|
||||
}
|
||||
if $result.valid { print "✅ Workflow is valid" } else {
|
||||
print "❌ Workflow validation failed"
|
||||
print $"Errors: ($result.errors | str join '\n ')"
|
||||
}
|
||||
}
|
||||
"monitor" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow ID required"; exit 1 }
|
||||
if $quiet {
|
||||
batch monitor $workflow_param --interval $interval --timeout $timeout --quiet
|
||||
} else {
|
||||
batch monitor $workflow_param --interval $interval --timeout $timeout
|
||||
}
|
||||
}
|
||||
"rollback" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow ID required"; exit 1 }
|
||||
let result = if ($checkpoint | is-not-empty) {
|
||||
batch rollback $workflow_param --checkpoint $checkpoint --force
|
||||
} else if $force {
|
||||
batch rollback $workflow_param --force
|
||||
} else {
|
||||
batch rollback $workflow_param
|
||||
}
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) }
|
||||
}
|
||||
"cancel" => {
|
||||
if ($workflow_param | is-empty) { print "❌ Workflow ID required"; exit 1 }
|
||||
let result = if ($reason | is-not-empty) and $force {
|
||||
batch cancel $workflow_param --reason $reason --force
|
||||
} else if ($reason | is-not-empty) {
|
||||
batch cancel $workflow_param --reason $reason
|
||||
} else if $force {
|
||||
batch cancel $workflow_param --force
|
||||
} else {
|
||||
batch cancel $workflow_param
|
||||
}
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) }
|
||||
}
|
||||
"template" => {
|
||||
let action = if ($workflow_param | is-not-empty) { $workflow_param } else { "list" }
|
||||
let tpl_name = ($ops | get 1? | default "")
|
||||
let result = match $action {
|
||||
"list" => { batch template "list" }
|
||||
"show" => { if ($tpl_name | is-empty) { print "❌ Template name required"; exit 1 }; batch template "show" $tpl_name }
|
||||
"delete" => { if ($tpl_name | is-empty) { print "❌ Template name required"; exit 1 }; batch template "delete" $tpl_name }
|
||||
"create" => {
|
||||
if ($tpl_name | is-empty) or ($from_file | is-empty) { print "❌ Name and --from-file required"; exit 1 }
|
||||
batch template "create" $tpl_name --from-file $from_file --description $description
|
||||
}
|
||||
_ => { print $"❌ Unknown template action: ($action)"; exit 1 }
|
||||
}
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) } else { print ($result | table) }
|
||||
}
|
||||
"stats" => {
|
||||
let result = if $detailed {
|
||||
batch stats --period $period --environment $environment --detailed
|
||||
} else {
|
||||
batch stats --period $period --environment $environment
|
||||
}
|
||||
if ($out | is-not-empty) and $out == "json" { print ($result | to json) }
|
||||
}
|
||||
"health" => {
|
||||
batch health
|
||||
}
|
||||
"help" | "h" => {
|
||||
print "Batch Workflow Management"
|
||||
print "Usage: provisioning batch <command> [args]"
|
||||
print ""
|
||||
print "Commands: list, status, submit, validate, monitor, rollback, cancel, template, stats, health"
|
||||
}
|
||||
"" => {
|
||||
print "❌ Batch subcommand required"
|
||||
print "Commands: list, status, submit, validate, monitor, rollback, cancel, template, stats, health"
|
||||
exit 1
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown batch command: ($task)"
|
||||
print "Commands: list, status, submit, validate, monitor, rollback, cancel, template, stats, health"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
54
nulib/cli/bootstrap.nu
Normal file
54
nulib/cli/bootstrap.nu
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for bootstrap command (~94ms vs ~9s through the full dispatcher).
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
use orchestration/bootstrap.nu *
|
||||
|
||||
def main [
|
||||
subcommand?: string # Optional: sync-firewall, sync-alias-ips
|
||||
--workspace (-w): string
|
||||
--dry-run (-n)
|
||||
--debug (-x)
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let ws = ($workspace | default "")
|
||||
match $subcommand {
|
||||
"sync-firewall" => {
|
||||
if $dry_run {
|
||||
if ($ws | is-not-empty) { main bootstrap sync-firewall --workspace $ws --dry-run } else { main bootstrap sync-firewall --dry-run }
|
||||
} else {
|
||||
if ($ws | is-not-empty) { main bootstrap sync-firewall --workspace $ws } else { main bootstrap sync-firewall }
|
||||
}
|
||||
}
|
||||
"sync-alias-ips" => {
|
||||
if $dry_run {
|
||||
if ($ws | is-not-empty) { main bootstrap sync-alias-ips --workspace $ws --dry-run } else { main bootstrap sync-alias-ips --dry-run }
|
||||
} else {
|
||||
if ($ws | is-not-empty) { main bootstrap sync-alias-ips --workspace $ws } else { main bootstrap sync-alias-ips }
|
||||
}
|
||||
}
|
||||
null | "" => {
|
||||
if $dry_run {
|
||||
if ($ws | is-not-empty) { main bootstrap --workspace $ws --dry-run } else { main bootstrap --dry-run }
|
||||
} else {
|
||||
if ($ws | is-not-empty) { main bootstrap --workspace $ws } else { main bootstrap }
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error make { msg: $"Unknown subcommand '($subcommand)'. Available: sync-firewall, sync-alias-ips" }
|
||||
}
|
||||
}
|
||||
}
|
||||
48
nulib/cli/build_prov.nu
Normal file
48
nulib/cli/build_prov.nu
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for build commands.
|
||||
# Loads only commands/build.nu.
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use cli/handlers/build.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--out: string = ""
|
||||
--debug (-x)
|
||||
--yes (-y)
|
||||
--check (-c)
|
||||
--notitles
|
||||
--verbose
|
||||
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let cmd = ($args | get 0? | default "")
|
||||
let ops = ($args | skip 1 | str join " ")
|
||||
let flags = (parse_common_flags {
|
||||
debug: $debug, out: ($out | default ""), notitles: $notitles,
|
||||
infra: ($infra | default ""), yes: $yes, check: $check, verbose: $verbose
|
||||
})
|
||||
handle_build_command $cmd $ops $flags
|
||||
}
|
||||
139
nulib/cli/catalog.nu
Normal file
139
nulib/cli/catalog.nu
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env nu
|
||||
# prvng catalog — browse IaC building blocks in catalog/.
|
||||
# Lists components, providers, taskservs, clusters, and domain artifacts.
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
def cat-root [] {
|
||||
let prov = ($env.PROVISIONING? | default "")
|
||||
if ($prov | is-empty) {
|
||||
error make --unspanned { msg: "PROVISIONING env var not set" }
|
||||
}
|
||||
# Post-constellation-migration: env.nu may point to the constellation root
|
||||
# while the catalog lives under code/. Try direct path first, then code/ sub-repo.
|
||||
let direct = ($prov | path join "catalog")
|
||||
if ($direct | path exists) { return $direct }
|
||||
let via_code = ($prov | path join "code" "catalog")
|
||||
if ($via_code | path exists) { return $via_code }
|
||||
error make --unspanned { msg: $"catalog not found under ($prov) — check PROVISIONING" }
|
||||
}
|
||||
|
||||
def list-catalog-dir [subdir: string]: nothing -> list {
|
||||
let dir = (cat-root | path join $subdir)
|
||||
if not ($dir | path exists) { return [] }
|
||||
ls $dir
|
||||
| where type == "dir"
|
||||
| get name
|
||||
| each { |p| $p | path basename }
|
||||
}
|
||||
|
||||
def cmd-list [args: list<string>] {
|
||||
let kind = $args | get 0? | default "all"
|
||||
let all_kinds = ["components" "providers" "taskservs" "domains" "playbooks" "workflows"]
|
||||
match $kind {
|
||||
"components" | "comp" | "c" => {
|
||||
let items = (list-catalog-dir "components")
|
||||
if ($items | is-empty) { print "No components found" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"providers" | "prov" => {
|
||||
let items = (list-catalog-dir "providers")
|
||||
if ($items | is-empty) { print "No providers found" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"taskservs" | "ts" => {
|
||||
let items = (list-catalog-dir "taskservs")
|
||||
if ($items | is-empty) { print "No taskservs found" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"domains" => {
|
||||
let items = (list-catalog-dir "domains")
|
||||
if ($items | is-empty) { print "No local domain artifacts" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"playbooks" | "pb" => {
|
||||
let items = (list-catalog-dir "playbooks")
|
||||
if ($items | is-empty) { print "No playbooks found" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"workflows" | "wf" => {
|
||||
let items = (list-catalog-dir "workflows")
|
||||
if ($items | is-empty) { print "No workflows found" } else {
|
||||
for n in $items { print $" ($n)" }
|
||||
}
|
||||
}
|
||||
"all" | _ => {
|
||||
for k in $all_kinds {
|
||||
let items = (list-catalog-dir $k)
|
||||
if ($items | is-not-empty) {
|
||||
print $"($k):"
|
||||
$items | each { |n| print $" ($n)" }
|
||||
print ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def cmd-show [args: list<string>] {
|
||||
let name = $args | get 0? | default ""
|
||||
if ($name | is-empty) {
|
||||
error make --unspanned { msg: "Usage: catalog show <name>" }
|
||||
}
|
||||
let root = (cat-root)
|
||||
for kind in ["components" "providers" "taskservs" "domains" "playbooks" "workflows"] {
|
||||
let dir = ($root | path join $kind $name)
|
||||
if ($dir | path exists) {
|
||||
print $"($kind)/($name)"
|
||||
for f in (ls $dir | get name) { print $" ($f | path basename)" }
|
||||
return
|
||||
}
|
||||
}
|
||||
error make --unspanned { msg: $"'($name)' not found in catalog" }
|
||||
}
|
||||
|
||||
def main [...args: string]: nothing -> nothing {
|
||||
let cmd = $args | get 0? | default ""
|
||||
let rest = if ($args | length) > 1 { $args | skip 1 } else { [] }
|
||||
match $cmd {
|
||||
"catalog" | "cat" => {
|
||||
let sub = $rest | get 0? | default "list"
|
||||
let sub_rest = if ($rest | length) > 1 { $rest | skip 1 } else { [] }
|
||||
match $sub {
|
||||
"list" | "ls" => { cmd-list $sub_rest }
|
||||
"show" => { cmd-show $sub_rest }
|
||||
_ => {
|
||||
print "prvng catalog <subcommand>"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " list [components|providers|taskservs|domains|playbooks|workflows] List catalog items"
|
||||
print " show <name> Show item files"
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print "prvng catalog <subcommand>"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " list [components|providers|taskservs|domains|playbooks|workflows]"
|
||||
print " show <name>"
|
||||
}
|
||||
}
|
||||
}
|
||||
411
nulib/cli/cli_cmd.nu
Normal file
411
nulib/cli/cli_cmd.nu
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
#!/usr/bin/env nu
|
||||
# Single CLI entry — replaces legacy nulib/provisioning runner (ADR-025 Phase 4).
|
||||
#
|
||||
# Single-route architecture: every command goes through dispatch_command, which
|
||||
# lazy-loads per-domain handlers on demand. The star-imports that dominated
|
||||
# cold-start in the legacy runner are gone; only the dispatcher surface + a
|
||||
# handful of init helpers are parsed on startup.
|
||||
#
|
||||
# Daemon and cache become orthogonal concerns applied INSIDE handlers (or their
|
||||
# lazy dependencies), not separate routes.
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
|
||||
let default_paths = [
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
]
|
||||
|
||||
$env.NU_LIB_DIRS = ($default_paths | append $current_lib_dirs)
|
||||
|
||||
if ( (version).installed_plugins | str contains "tera" ) {
|
||||
(plugin use tera)
|
||||
}
|
||||
|
||||
# Bash exports booleans as strings — normalize before any module code runs.
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_no_terminal = ($env.PROVISIONING_NO_TERMINAL? | default "")
|
||||
if ($raw_no_terminal | describe) == "string" and ($raw_no_terminal | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TERMINAL = (do $_coerce $raw_no_terminal)
|
||||
}
|
||||
let raw_titles_shown = ($env.PROVISIONING_TITLES_SHOWN? | default "")
|
||||
if ($raw_titles_shown | describe) == "string" and ($raw_titles_shown | is-not-empty) {
|
||||
$env.PROVISIONING_TITLES_SHOWN = (do $_coerce $raw_titles_shown)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
|
||||
if ($env.NOW? | is-empty) {
|
||||
$env.NOW = (date now | format date "%Y_%m_%d_%H_%M_%S")
|
||||
}
|
||||
|
||||
if ($env.SSH_OPS? | is-empty) {
|
||||
$env.SSH_OPS = [StrictHostKeyChecking=accept-new UserKnownHostsFile=/dev/null]
|
||||
}
|
||||
}
|
||||
|
||||
# ADR-025 Phase 4 perf insight: Nushell selective imports (`use x [sym]`) still
|
||||
# parse the entire source module. To actually defer parse cost we must move
|
||||
# `use` statements INSIDE function bodies — they're then evaluated only when
|
||||
# the function is called, not at file-parse time. Parsing this file itself
|
||||
# only sees two `def` headers and one `export-env` block.
|
||||
|
||||
# Pass-through: Nushell parameter parsing handles interleaved flags, so we
|
||||
# just return args as-is. Preserved as a seam for future normalization.
|
||||
def reorder_args [args: list]: nothing -> list { $args }
|
||||
|
||||
export def "main help" [
|
||||
...args: string
|
||||
--notitles
|
||||
--out: string
|
||||
] {
|
||||
use primitives/infra/init.nu [show_titles]
|
||||
use primitives/io/interface.nu [end_run]
|
||||
use orchestration/ops.nu [provisioning_options]
|
||||
|
||||
if $notitles == null or not $notitles { show_titles }
|
||||
if ($out | is-not-empty) { $env.PROVISIONING_NO_TERMINAL = false }
|
||||
let category = if ($args | length) > 0 { ($args | get 0) } else { "" }
|
||||
print (provisioning_options $category)
|
||||
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
|
||||
}
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string
|
||||
--settings (-s): string
|
||||
--serverpos (-p): int
|
||||
--outfile (-o): string
|
||||
--template(-t): string
|
||||
--check (-c)
|
||||
--upload (-u)
|
||||
--yes (-y)
|
||||
--wait
|
||||
--keepstorage
|
||||
--select: string
|
||||
--onsel: string
|
||||
--infras: string
|
||||
--new (-n): string
|
||||
--debug (-x)
|
||||
--xm
|
||||
--xc
|
||||
--xr
|
||||
--xld
|
||||
--nc
|
||||
--metadata
|
||||
--notitles
|
||||
--environment: string
|
||||
--dep-option: string
|
||||
--dep-url: string
|
||||
--dry-run
|
||||
--force (-f)
|
||||
--all
|
||||
--keep-latest: int
|
||||
--workspace (-w): string
|
||||
--activate
|
||||
--interactive
|
||||
--org: string
|
||||
--apply
|
||||
--verbose
|
||||
--pretty
|
||||
-v
|
||||
--version (-V)
|
||||
--info
|
||||
--about
|
||||
--helpinfo (-h)
|
||||
--out: string
|
||||
--view
|
||||
--inputfile: string
|
||||
--include_notuse
|
||||
--services: string
|
||||
]: nothing -> nothing {
|
||||
# Function-local imports: parsed only when main() is called, not at
|
||||
# file-parse time. Keeps cold-start for help-like shortcuts minimal.
|
||||
use primitives/io/interface.nu [_ansi _print end_run]
|
||||
use primitives/infra/init.nu [provisioning_init]
|
||||
use primitives/defs/about.nu [about_info]
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use orchestration/ops.nu [provisioning_options]
|
||||
use cli/dispatcher.nu [dispatch_command]
|
||||
|
||||
let reordered_args = (reorder_args $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"})
|
||||
|
||||
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)
|
||||
|
||||
provisioning_init $helpinfo "" $reordered_args
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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 (about_info) ; exit }
|
||||
|
||||
let is_help_command = (
|
||||
($reordered_args | length) == 0 or
|
||||
($reordered_args | get 0) in [
|
||||
"help", "-h", "--help",
|
||||
"sc", "shortcuts", "quickstart", "quick",
|
||||
"from-scratch", "scratch",
|
||||
"customize", "custom",
|
||||
"guide", "guides", "howto",
|
||||
"setup", "st",
|
||||
"workspace", "ws",
|
||||
"mod", "module", "discover", "disc",
|
||||
"dt", "dp", "dc",
|
||||
"discover-taskservs", "disc-t",
|
||||
"discover-providers", "disc-p",
|
||||
"discover-clusters", "disc-c",
|
||||
"lyr", "layer", "version", "pack",
|
||||
"nuinfo", "env", "allenv",
|
||||
"validate", "val", "show", "config-template",
|
||||
"cache",
|
||||
"list", "l", "ls",
|
||||
"plugin", "plugins",
|
||||
"qr", "ssh", "sops",
|
||||
"providers",
|
||||
"status", "health", "diagnostics", "next", "phase"
|
||||
]
|
||||
)
|
||||
|
||||
let skip_bootstrap = (
|
||||
(($reordered_args | length) > 0 and
|
||||
($reordered_args | get 0) in [
|
||||
"nu",
|
||||
"platform", "plat", "p",
|
||||
"vm", "vmi", "vmh", "vml",
|
||||
"server", "s",
|
||||
"taskserv", "task", "t",
|
||||
"cluster", "cl",
|
||||
"bootstrap",
|
||||
"create", "c",
|
||||
"delete", "d",
|
||||
"update", "u",
|
||||
"build", "b", "bi", "build-image"
|
||||
]) or
|
||||
$final_check or
|
||||
($env.PROVISIONING_MODULE? | default "" | is-not-empty)
|
||||
)
|
||||
|
||||
if (not $is_help_command) and (not $skip_bootstrap) {
|
||||
use 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
|
||||
}
|
||||
}
|
||||
|
||||
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||
print $"DEBUG provisioning-cli: reordered_args = ($reordered_args)" >&2
|
||||
print $"DEBUG provisioning-cli: parsed_flags.infra = (($parsed_flags | get -o infra | default 'MISSING'))" >&2
|
||||
}
|
||||
|
||||
# Help: short-circuit before dispatcher to avoid recursive exec loops.
|
||||
if (($reordered_args | length) > 0) and (($reordered_args | get 0) in ["help" "h"]) {
|
||||
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
|
||||
}
|
||||
|
||||
# Info/discovery/utility commands bypass workspace enforcement.
|
||||
if (($reordered_args | length) > 0) and (($reordered_args | get 0) in [
|
||||
"guide", "guides", "sc", "howto", "shortcuts", "quickstart", "quick",
|
||||
"from-scratch", "scratch", "customize", "custom",
|
||||
"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",
|
||||
"plugin", "plugins",
|
||||
"qr", "nuinfo",
|
||||
"status", "health", "diagnostics", "next", "phase"
|
||||
]) {
|
||||
dispatch_command $reordered_args $parsed_flags
|
||||
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
|
||||
return
|
||||
}
|
||||
|
||||
# -mod <module> mode: bash wrapper extracts `-mod <name>` into
|
||||
# PROVISIONING_MODULE and forwards remaining args. We invoke that module's
|
||||
# `main` directly, bypassing the dispatcher.
|
||||
if ($env.PROVISIONING_MODULE? | default "" | is-not-empty) {
|
||||
let module = $env.PROVISIONING_MODULE
|
||||
|
||||
match $module {
|
||||
"server" => {
|
||||
use orchestration/servers/create.nu *
|
||||
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
|
||||
}
|
||||
}
|
||||
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" => {
|
||||
let ts_subcmd = if ($reordered_args | length) > 0 { $reordered_args | first } else { "create" }
|
||||
match $ts_subcmd {
|
||||
"status" | "s" => {
|
||||
use orchestration/taskservs/status.nu *
|
||||
main status --infra=($infra | default "") --settings=($settings | default "")
|
||||
}
|
||||
"list" | "l" => {
|
||||
use orchestration/taskservs/status.nu *
|
||||
main list --infra=($infra | default "") --settings=($settings | default "") --out=($out | default "")
|
||||
}
|
||||
# "." sentinel: full-formula run for a server (task_name empty, server = remaining[0])
|
||||
# NOTE: must not use ...$remaining to pass server — Nu spread bypasses optional
|
||||
# positional binding (server?) and lands in ...args instead.
|
||||
"." => {
|
||||
use orchestration/taskservs/create.nu *
|
||||
let remaining = ($reordered_args | skip 1)
|
||||
let srv = ($remaining | first | default "")
|
||||
let extra = ($remaining | skip 1)
|
||||
main create "." $srv ...$extra --check=$final_check --upload=$final_upload --wait=$final_wait --infra=($infra | default "") --settings=($settings | default "") --debug=$debug
|
||||
}
|
||||
# Component name passed directly (e.g. `provisioning -mod taskserv os hostname`).
|
||||
# Same spread-bypass fix as ".": bind task_name and server explicitly.
|
||||
$comp if ($comp not-in ["create", "c", "update", "u", "delete", "d", "h", "help"]) => {
|
||||
use orchestration/taskservs/create.nu *
|
||||
let remaining = ($reordered_args | skip 1)
|
||||
let srv = ($remaining | first | default "")
|
||||
let extra = ($remaining | skip 1)
|
||||
main create $comp $srv ...$extra --check=$final_check --upload=$final_upload --wait=$final_wait --infra=($infra | default "") --settings=($settings | default "") --debug=$debug
|
||||
}
|
||||
_ => {
|
||||
use orchestration/taskservs/create.nu *
|
||||
main ...$reordered_args --check=$final_check --upload=$final_upload --wait=$final_wait --debug=$debug
|
||||
}
|
||||
}
|
||||
}
|
||||
"cluster" => {
|
||||
use orchestration/clusters/create.nu *
|
||||
main ...$reordered_args --check=$final_check --debug=$debug
|
||||
}
|
||||
"images" => {
|
||||
use orchestration/images/create.nu *
|
||||
use orchestration/images/list.nu *
|
||||
use orchestration/images/update.nu *
|
||||
use orchestration/images/delete.nu *
|
||||
use orchestration/images/state.nu *
|
||||
use orchestration/images/watch.nu *
|
||||
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 {
|
||||
dispatch_command $reordered_args $parsed_flags
|
||||
}
|
||||
|
||||
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
|
||||
}
|
||||
166
nulib/cli/cluster.nu
Normal file
166
nulib/cli/cluster.nu
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for cluster commands.
|
||||
# Loads only cluster-deploy.nu + discover + workspace (~140ms vs ~49s for the full entry).
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
use domain/workspace/mod.nu *
|
||||
use platform/user/config.nu [get-workspace-path, get-active-workspace-details]
|
||||
use platform/config/accessor.nu [config-get]
|
||||
use orchestration/cluster_deploy.nu *
|
||||
use orchestration/clusters/discover.nu [discover-clusters, get-cluster-info]
|
||||
use cli/components.nu ["main component list", "main cluster top", "main cluster label-nodes", "main cluster plan", "main cluster capacity", "main cluster services"]
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--workspace (-w): string = ""
|
||||
--dry-run (-n)
|
||||
--kubeconfig (-k): string = ""
|
||||
--secrets-file (-s): string = ""
|
||||
--out (-o): string = ""
|
||||
--debug (-x)
|
||||
--notitles
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
|
||||
let rest = if (($args | length) > 0) and (($args | first) in ["cluster", "cl"]) {
|
||||
$args | skip 1
|
||||
} else {
|
||||
$args
|
||||
}
|
||||
let sub = ($rest | get 0? | default "")
|
||||
let arg1 = ($rest | get 1? | default "")
|
||||
let arg2 = ($rest | get 2? | default "")
|
||||
|
||||
let help_text = {
|
||||
print "Cluster Management"
|
||||
print "=================="
|
||||
print ""
|
||||
print "Usage: prvng cluster <subcommand> [options]"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " list List cluster-mode components in active workspace (alias: ls, l)"
|
||||
print " show <name> Show a cluster-mode component's full config (alias: sh, info)"
|
||||
print " top [pods] [-o json|yaml] Live node usage via kubectl top; (alias: t)"
|
||||
print " 'top pods' appends heaviest pods by memory"
|
||||
print " '-o json|yaml' emits structured output + legend on req%/lim%/no-req"
|
||||
print " services [-n <ns>] [-o json|yaml] List all Kubernetes services in the cluster (alias: svc, svcs)"
|
||||
print " label-nodes [--apply] Reconcile declared node labels/node_class (alias: labels, ln)"
|
||||
print " onto live nodes (dry-run unless --apply)"
|
||||
print " plan <service> [-o json|yaml] Simulate a (re)deploy: projected req%/ws% (alias: p, sim)"
|
||||
print " per eligible node + fit recommendation"
|
||||
print " capacity [--add-worker] [--resize <n>] Per-node load by category (movable/pinned/ (alias: cap)"
|
||||
print " infra) + horizontal-vs-vertical what-if"
|
||||
print " deploy <layer> <cluster> [options] Deploy a cluster (layer: platform|apps) (alias: d)"
|
||||
print ""
|
||||
print "Flags for deploy:"
|
||||
print " --workspace (-w) <name> Workspace name (defaults to active)"
|
||||
print " --dry-run (-n) Print execution plan without running scripts"
|
||||
print " --kubeconfig (-k) <path> Override KUBECONFIG path"
|
||||
print " --secrets-file (-s) <path> SOPS-encrypted dotenv with install secrets"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " prvng cl l"
|
||||
print " prvng cluster show sgoyol"
|
||||
print " prvng cl deploy platform sgoyol --dry-run"
|
||||
print " prvng cluster deploy apps wuji -w libre-daoshi"
|
||||
}
|
||||
|
||||
match $sub {
|
||||
"" | "help" | "h" | "-h" | "--help" => { do $help_text }
|
||||
"list" | "ls" | "l" => {
|
||||
let rows = if ($workspace | is-not-empty) {
|
||||
main component list --workspace $workspace --mode "cluster"
|
||||
} else {
|
||||
main component list --mode "cluster"
|
||||
}
|
||||
if ($rows | is-empty) {
|
||||
print "No cluster-mode components declared in the active workspace."
|
||||
return
|
||||
}
|
||||
$rows | reject pod_selector
|
||||
}
|
||||
"show" | "info" | "sh" => {
|
||||
if ($arg1 | is-empty) {
|
||||
print "Error: cluster show requires a name"
|
||||
print "Usage: prvng cluster show <name>"
|
||||
return
|
||||
}
|
||||
let rows = if ($workspace | is-not-empty) {
|
||||
main component list --workspace $workspace --mode "cluster"
|
||||
} else {
|
||||
main component list --mode "cluster"
|
||||
}
|
||||
let found = ($rows | where name == $arg1 | get 0?)
|
||||
if ($found | is-empty) {
|
||||
print $"Error: cluster component '($arg1)' not found in active workspace"
|
||||
print $"Available: ($rows | get name | str join ', ')"
|
||||
return
|
||||
}
|
||||
$found | reject pod_selector live_check
|
||||
}
|
||||
"services" | "svc" | "svcs" => {
|
||||
let ns_arg = if ($arg1 | is-not-empty) and ($arg1 | str starts-with "-") == false { $arg1 } else { "" }
|
||||
let out_val = if ($arg1 == "-o") { $arg2 } else if ($arg2 == "-o") { "" } else { $out }
|
||||
let ws_flag = if ($workspace | is-not-empty) { [$workspace] } else { [] }
|
||||
if ($ns_arg | is-not-empty) {
|
||||
main cluster services --workspace ($ws_flag | get 0? | default "") --namespace $ns_arg --out $out_val
|
||||
} else {
|
||||
main cluster services --workspace ($ws_flag | get 0? | default "") --out $out_val
|
||||
}
|
||||
}
|
||||
"top" | "t" => {
|
||||
main cluster top $arg1 --workspace $workspace --out $out
|
||||
}
|
||||
"label-nodes" | "labels" | "ln" => {
|
||||
let apply = ($arg1 in ["apply", "--apply", "-a"])
|
||||
if $apply {
|
||||
main cluster label-nodes --workspace $workspace --apply
|
||||
} else {
|
||||
main cluster label-nodes --workspace $workspace
|
||||
}
|
||||
}
|
||||
"plan" | "p" | "sim" => {
|
||||
if ($arg1 | is-empty) {
|
||||
print "Usage: prvng cluster plan <service> [-o json|yaml]"
|
||||
return
|
||||
}
|
||||
main cluster plan $arg1 --workspace $workspace --out $out
|
||||
}
|
||||
"capacity" | "cap" => {
|
||||
main cluster capacity --workspace $workspace --out $out
|
||||
}
|
||||
"deploy" | "d" => {
|
||||
let layer = $arg1
|
||||
let cluster = $arg2
|
||||
if ($layer | is-empty) or ($cluster | is-empty) {
|
||||
print "Usage: prvng cluster deploy <layer> <cluster> [--workspace <name>]"
|
||||
print " layer: platform | apps"
|
||||
print " cluster: sgoyol | wuji | ..."
|
||||
exit 1
|
||||
}
|
||||
if ($workspace | is-not-empty) {
|
||||
main cluster deploy $layer $cluster --workspace $workspace --dry-run=$dry_run --kubeconfig $kubeconfig --secrets-file $secrets_file
|
||||
} else {
|
||||
main cluster deploy $layer $cluster --dry-run=$dry_run --kubeconfig $kubeconfig --secrets-file $secrets_file
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print $"Unknown cluster subcommand: ($sub)"
|
||||
print ""
|
||||
do $help_text
|
||||
}
|
||||
}
|
||||
}
|
||||
736
nulib/cli/component.nu
Normal file
736
nulib/cli/component.nu
Normal file
|
|
@ -0,0 +1,736 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for component commands.
|
||||
# Bypasses full dispatcher — loads only components/mod.nu + targeted lib_provisioning.
|
||||
# Mirrors the provisioning-taskserv.nu pattern for <1s startup.
|
||||
|
||||
export-env {
|
||||
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
|
||||
}
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
}
|
||||
|
||||
use primitives/io/interface.nu [_ansi]
|
||||
|
||||
use cli/components.nu [
|
||||
"main component list"
|
||||
"main component info"
|
||||
"main component mark-complete"
|
||||
"main component bulk-mark-complete"
|
||||
"main component drop-key"
|
||||
"main component drop-server"
|
||||
"main component state-check"
|
||||
"main component state-normalize"
|
||||
"main validate capabilities"
|
||||
"main validate components"
|
||||
"main component install"
|
||||
"main component delete"
|
||||
"main component update"
|
||||
"main component reinstall"
|
||||
"main component restart"
|
||||
"main component backup"
|
||||
"main component restore"
|
||||
"main component check-updates"
|
||||
"main component render"
|
||||
comp-live-check
|
||||
comp-pods-for
|
||||
comp-ssh-pods
|
||||
comp-ssh-k8s-data
|
||||
comp-ssh-describe-pod
|
||||
comp-state-server
|
||||
comp-kubectl-access
|
||||
comp-ws-path
|
||||
comp-ws-infra
|
||||
comp-load-component-record
|
||||
comp-load-server-info
|
||||
comp-server-badge
|
||||
comp-cluster-summary
|
||||
comp-cluster-badge
|
||||
comp-cache-last-sync
|
||||
comp-settings-k0s-version
|
||||
comp-ncl-export
|
||||
]
|
||||
|
||||
def state-color [s: string]: nothing -> string {
|
||||
match $s {
|
||||
"completed" => $"(_ansi green)($s)(_ansi reset)"
|
||||
"running" => $"(_ansi yellow)($s)(_ansi reset)"
|
||||
"failed" => $"(_ansi red)($s)(_ansi reset)"
|
||||
"blocked" => $"(_ansi red)($s)(_ansi reset)"
|
||||
"partial" => $"(_ansi yellow)($s)(_ansi reset)"
|
||||
_ => $"(_ansi dark_gray)($s)(_ansi reset)"
|
||||
}
|
||||
}
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--workspace (-w): string = ""
|
||||
--mode: string = ""
|
||||
--out (-o): string = ""
|
||||
--infra (-i): string = ""
|
||||
--server (-s): string = ""
|
||||
--op: string = "init" # render: which op's plan scripts to include
|
||||
--bundle # render: also write <name>.tar.gz.b64
|
||||
--check
|
||||
--dry-run # alias for --check: validates bundle without submitting
|
||||
--wait
|
||||
--live
|
||||
--ext
|
||||
--json (-j)
|
||||
--debug (-x)
|
||||
--filter (-f): string = "" # substring on name, or col=val (state=failed, ns=monitoring)
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = "true" }
|
||||
let effective_check = ($check or $dry_run)
|
||||
|
||||
# Args come in as: ["component", "ls"] or ["ls", "postgresql"] depending on dispatch
|
||||
let rest = if (($args | length) > 0) and (($args | first) in ["component", "comp", "c"]) {
|
||||
$args | skip 1
|
||||
} else {
|
||||
$args
|
||||
}
|
||||
let sub = ($rest | get 0? | default "")
|
||||
let name = ($rest | get 1? | default "")
|
||||
|
||||
# Workspace resolution: explicit flag > active env > empty (ext_only view)
|
||||
let ws = if ($workspace | is-not-empty) {
|
||||
$workspace
|
||||
} else {
|
||||
$env.PROVISIONING_KLOUD? | default ""
|
||||
}
|
||||
|
||||
let help_text = {
|
||||
print "Component Management"
|
||||
print "===================="
|
||||
print ""
|
||||
print "Usage: prvng component <subcommand> [options]"
|
||||
print ""
|
||||
print "Read operations:"
|
||||
print " list [--workspace <ws>] [--live] Provisioning state per component (alias: ls, l)"
|
||||
print " ll [--workspace <ws>] Same as list --live (alias for --live)"
|
||||
print " lc [--workspace <ws>] Same as list --mode cluster (alias: list cluster)"
|
||||
print " lcl [--workspace <ws>] Same as list --mode cluster --live (alias: lc + live)"
|
||||
print " [--server <host>] Filter taskserv rows to one server"
|
||||
print " [<host>] Positional server filter (same as --server)"
|
||||
print " [--mode cluster|taskserv] Filter by mode"
|
||||
print " [--filter <text>] Substring match on name (e.g. --filter prom)"
|
||||
print " [--filter col=val] Per-column match: state=failed, ns=monitoring, srv=wrk-0"
|
||||
print " [--json] Raw JSON output (flat table)"
|
||||
print " show <name> [--workspace <ws>] [--live] Full definition + state history (alias: sh, info)"
|
||||
print " status <name> [--workspace <ws>] [--live] Operational snapshot + state (alias: s, st)"
|
||||
print ""
|
||||
print "State column values:"
|
||||
print " pending Task not yet started (or server never initialized)"
|
||||
print " running Task in progress — or stuck if ended_at is empty"
|
||||
print " completed Task finished successfully via orchestrator"
|
||||
print " failed Task finished with error"
|
||||
print " — Component not in any DAG formula and no state entry yet"
|
||||
print ""
|
||||
print "Write operations (require orchestrator):"
|
||||
print " install <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
|
||||
print " delete <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
|
||||
print " update <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
|
||||
print " reinstall <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
|
||||
print " restart <name> [--workspace <ws>] [--server <host>] [--wait]"
|
||||
print " backup <name> [--workspace <ws>] [--server <host>] [--wait]"
|
||||
print " restore <name> [--workspace <ws>] [--server <host>] [--check] [--wait]"
|
||||
print " check-updates <name> [--workspace <ws>] [--server <host>]"
|
||||
print ""
|
||||
print "State correction:"
|
||||
print " mark-complete <name> [--server <host>] [--note <text>]"
|
||||
print " Force state → completed with actor attribution. Use when:"
|
||||
print " • Component deployed manually or by an agent without orchestrator callback"
|
||||
print " • State stuck in 'running (ended_at empty) after successful deploy"
|
||||
print " • Task completed via human intervention and needs verification record"
|
||||
print " Alias: mc"
|
||||
print ""
|
||||
print " mark-complete-live [--workspace <ws>]"
|
||||
print " Bulk mark-complete for all components where state=running AND live=healthy."
|
||||
print " Fetches live k8s data (one SSH session) before deciding — only marks what"
|
||||
print " is confirmed running by the live check strategy."
|
||||
print " Alias: mcl"
|
||||
print ""
|
||||
print " mark-complete-all [--workspace <ws>]"
|
||||
print " Bulk mark-complete for ALL components where state=running, no live check."
|
||||
print " Use for components without live_check (os, vol_prepare, kubernetes, etc.)"
|
||||
print " or when k8s is unreachable and state is known to be correct."
|
||||
print " Alias: mca"
|
||||
print ""
|
||||
print " bulk-mark-complete [--include-failed] [--dry-run] [--note <text>]"
|
||||
print " Single-write sweep: marks all state=running (ended_at empty) as completed."
|
||||
print " --include-failed: also sweep state=failed entries (e.g. cilium/longhorn that"
|
||||
print " failed on first attempt but are actually running — verify pods first)."
|
||||
print " Alias: bmc"
|
||||
print ""
|
||||
print " state-check [--server <host>]"
|
||||
print " Diagnose state incoherence: lists stale-running, failed-but-recovered,"
|
||||
print " and duplicate kebab/snake keys. Use this first to understand what to fix."
|
||||
print " Alias: sc"
|
||||
print ""
|
||||
print " drop-key <key> --server <host> [--dry-run]"
|
||||
print " Remove a specific state key from a server's taskservs. Use to prune"
|
||||
print " orphaned duplicate entries left by naming migrations (e.g. odoo-jesusperez"
|
||||
print " coexisting with odoo_jesusperez)."
|
||||
print " Alias: dk"
|
||||
print ""
|
||||
print " state-normalize [--dry-run]"
|
||||
print " One-shot migration: merge all snake_case state keys into their kebab"
|
||||
print " equivalents. Eliminates duplicate entries from previous naming inconsistency."
|
||||
print " Run once, then verify with sc. Future writes always use kebab (fixed upstream)."
|
||||
print " Alias: sn"
|
||||
print ""
|
||||
print "Flags:"
|
||||
print " --live Fetch live pod status from k8s via SSH (k0s kubectl get pods)."
|
||||
print " Shows actual pod counts — not a proxy for provisioning state."
|
||||
print " --check / --dry-run Dry-run: validates bundle without submitting to orchestrator."
|
||||
print " --wait Block until orchestrator task reaches terminal state."
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " prvng c l # provisioning state"
|
||||
print " prvng c ll # + live pod status"
|
||||
print " prvng c lc # cluster components only"
|
||||
print " prvng c lcl # cluster components + live"
|
||||
print " prvng c l libre-wuji-wrk-0 # taskserv for one server"
|
||||
print " prvng c l --live --workspace libre-wuji"
|
||||
print " prvng c mc postgresql --note 'verified: kubectl get sts -n data'"
|
||||
print " prvng c install postgresql --wait"
|
||||
print " prvng c sh postgresql --live"
|
||||
}
|
||||
|
||||
match $sub {
|
||||
"" => { do $help_text }
|
||||
"list" | "ls" | "l" | "ll" | "lc" | "lcl" => {
|
||||
let is_live = $live or ($sub == "ll") or ($sub == "lcl")
|
||||
let effective_mode = if $sub in ["lc", "lcl"] {
|
||||
"cluster"
|
||||
} else if ($mode | is-not-empty) {
|
||||
$mode
|
||||
} else {
|
||||
""
|
||||
}
|
||||
# Second positional arg is treated as a server filter for list ops.
|
||||
let effective_server = if ($server | is-not-empty) { $server } else { $name }
|
||||
let all_rows = with-env { PROVISIONING_NO_CACHE: "true" } {
|
||||
if ($ws | is-not-empty) {
|
||||
main component list --workspace $ws --mode $effective_mode --server $effective_server
|
||||
} else {
|
||||
main component list --mode $effective_mode --server $effective_server
|
||||
}
|
||||
}
|
||||
|
||||
let filtered_rows = if ($filter | is-not-empty) {
|
||||
if ($filter | str contains "=") {
|
||||
let parts = ($filter | split row "=" | take 2)
|
||||
let col = ($parts | get 0)
|
||||
let val = ($parts | get 1 | str downcase)
|
||||
let col_map = { ns: "namespace", state: "state", deploy: "state", name: "name", srv: "server", server: "server" }
|
||||
let real_col = ($col_map | get -o $col | default $col)
|
||||
$all_rows | where { ($in | get -o $real_col | default "" | into string | str downcase) | str contains $val }
|
||||
} else {
|
||||
let term = ($filter | str downcase)
|
||||
$all_rows | where { $in.name | str downcase | str contains $term }
|
||||
}
|
||||
} else {
|
||||
$all_rows
|
||||
}
|
||||
let cluster_rows = ($filtered_rows | where mode == "cluster")
|
||||
let taskserv_rows = ($filtered_rows | where mode == "taskserv")
|
||||
|
||||
# Single SSH session for all live checks (cluster + taskserv).
|
||||
let ws_root_l = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
|
||||
let infra_l = (comp-ws-infra $ws_root_l)
|
||||
let state_root_l = if (($ws_root_l | path join "infra" $infra_l ".provisioning-state.ncl") | path exists) {
|
||||
$ws_root_l | path join "infra" $infra_l
|
||||
} else { $ws_root_l }
|
||||
let ssh_srv = if $is_live { (comp-kubectl-access $ws_root_l $infra_l).host } else { "" }
|
||||
let k8s_data = if $is_live {
|
||||
let systemd_svcs = ($filtered_rows
|
||||
| where { ($in | get -o live_check | default {} | get -o strategy | default "") == "systemd" }
|
||||
| each { $in | get -o live_check | default {} | get -o service | default "" }
|
||||
| where { $in | is-not-empty })
|
||||
comp-ssh-k8s-data $ws_root_l $infra_l $ssh_srv --systemd-services $systemd_svcs
|
||||
} else { {} }
|
||||
|
||||
# A taskserv's systemd service runs on the node it was deployed to — not the
|
||||
# CP. The single CP gather above only sees cp-0's units, so a worker-only
|
||||
# service (e.g. coredns-vpn-wrk1 on wrk-1) always shows inactive. Gather
|
||||
# systemd per-server for taskserv rows whose server is not the CP, so each is
|
||||
# checked on its own node. (live_check.scope is advisory; the deploy server is
|
||||
# authoritative for where a taskserv's unit actually runs.)
|
||||
let extra_systemd = if $is_live {
|
||||
$taskserv_rows
|
||||
| where { (($in | get -o live_check | default {} | get -o strategy | default "") == "systemd") and (($in.server? | default "") != $ssh_srv) and (($in.server? | default "") | is-not-empty) }
|
||||
| group-by server
|
||||
| items {|srv, rows|
|
||||
let svcs = ($rows | each { $in | get -o live_check | default {} | get -o service | default "" } | where { $in | is-not-empty } | uniq)
|
||||
{ server: $srv, systemd: ((comp-ssh-k8s-data $ws_root_l $infra_l $srv --systemd-services $svcs) | get -o systemd | default {}) }
|
||||
}
|
||||
| reduce --fold {} {|it, acc| $acc | insert $it.server $it.systemd }
|
||||
} else { {} }
|
||||
|
||||
# If live data could not be fetched (SSH/kubectl unreachable), every row
|
||||
# would misleadingly show "no pods". Warn once that it's a fetch failure,
|
||||
# not pod absence.
|
||||
if $is_live and ($k8s_data.pods | is-empty) and ($k8s_data.nodes | is-empty) {
|
||||
print $"(_ansi yellow)⚠ live cluster data unavailable \(CP: ($ssh_srv | default 'unresolved'))\) — 'live' column reflects the failed fetch, not pod absence(_ansi reset)"
|
||||
}
|
||||
|
||||
# Server info: cache by default, hcloud query when --live.
|
||||
let srv_info = if ($taskserv_rows | is-not-empty) or ($cluster_rows | is-not-empty) {
|
||||
if $is_live {
|
||||
comp-load-server-info $ws_root_l $infra_l --live
|
||||
} else {
|
||||
comp-load-server-info $ws_root_l $infra_l
|
||||
}
|
||||
} else { {} }
|
||||
|
||||
if $json {
|
||||
let components_out = ($filtered_rows | reject pod_selector live_check)
|
||||
let servers_out = ($srv_info | items {|k v| {key: $k, value: $v}}
|
||||
| reduce --fold {} {|it acc| $acc | insert $it.key $it.value})
|
||||
{components: $components_out, servers: $servers_out} | to json | print
|
||||
return
|
||||
}
|
||||
|
||||
# ── Cluster section ──────────────────────────────────────────
|
||||
if ($cluster_rows | is-not-empty) {
|
||||
let _op_ws = (comp-ncl-export $ws_root_l $"infra/($infra_l)/settings.ncl"
|
||||
| get -o settings | default {}
|
||||
| get -o operator_workspace | default "")
|
||||
let _cluster_header = if ($_op_ws | is-not-empty) {
|
||||
$"(_ansi white_bold)CLUSTER COMPONENTS(_ansi reset) (_ansi dark_gray)→ hosted on(_ansi reset) (_ansi cyan)($_op_ws)(_ansi reset)"
|
||||
} else {
|
||||
$"(_ansi white_bold)CLUSTER COMPONENTS(_ansi reset)"
|
||||
}
|
||||
print $_cluster_header
|
||||
print $"(_ansi dark_gray)────────────────────────────────────────────────────(_ansi reset)"
|
||||
let cl_version = (comp-settings-k0s-version $ws_root_l $infra_l)
|
||||
let cl_summary = if $is_live {
|
||||
comp-cluster-summary $ws_root_l $infra_l $k8s_data $cl_version --live
|
||||
} else {
|
||||
comp-cluster-summary $ws_root_l $infra_l {} $cl_version
|
||||
}
|
||||
let cl_badge = (comp-cluster-badge $cl_summary)
|
||||
if ($cl_badge | is-not-empty) {
|
||||
print $" (_ansi dark_gray)($cl_badge)(_ansi reset)"
|
||||
}
|
||||
if $is_live {
|
||||
$cluster_rows | each {|row|
|
||||
let check = (comp-live-check $row.name $row $k8s_data)
|
||||
let live_val = if $check.live == "—" {
|
||||
$"(_ansi dark_gray)($check.detail)(_ansi reset)"
|
||||
} else { $check.detail }
|
||||
$row | select name namespace version class resources placement state | update state {|r| state-color $r.state} | rename --column {state: deploy} | insert live $live_val
|
||||
} | table | print
|
||||
} else {
|
||||
$cluster_rows | select name namespace version class resources placement state | update state {|r| state-color $r.state} | rename --column {state: deploy} | table | print
|
||||
}
|
||||
print ""
|
||||
}
|
||||
|
||||
# ── Taskserv section ─────────────────────────────────────────
|
||||
if ($taskserv_rows | is-not-empty) {
|
||||
print $"(_ansi white_bold)TASKSERV COMPONENTS(_ansi reset)"
|
||||
let servers = ($taskserv_rows | get server | uniq | sort)
|
||||
for srv in $servers {
|
||||
print ""
|
||||
let info = ($srv_info | get -o $srv | default {})
|
||||
let badge = (comp-server-badge $info)
|
||||
if ($badge | is-not-empty) {
|
||||
print $" (_ansi white_bold)($srv)(_ansi reset) (_ansi dark_gray)($badge)(_ansi reset)"
|
||||
} else {
|
||||
print $" (_ansi white_bold)($srv)(_ansi reset)"
|
||||
}
|
||||
print $" (_ansi dark_gray)──────────────────────────────────────────────(_ansi reset)"
|
||||
let srv_rows = ($taskserv_rows | where server == $srv | sort-by name)
|
||||
if $is_live {
|
||||
# Use this server's own systemd state (gathered per-server above)
|
||||
# so a taskserv unit is checked on the node it runs on, not the CP.
|
||||
let srv_k8s = ($k8s_data | upsert systemd (if ($srv == $ssh_srv) { ($k8s_data.systemd? | default {}) } else { ($extra_systemd | get -o $srv | default {}) }))
|
||||
$srv_rows | each {|row|
|
||||
let check = (comp-live-check $row.name $row $srv_k8s)
|
||||
let live_val = if $check.live == "—" {
|
||||
$"(_ansi dark_gray)($check.detail)(_ansi reset)"
|
||||
} else { $check.detail }
|
||||
{ name: $row.name, deploy: (state-color $row.state), live: $live_val }
|
||||
} | table | str trim | lines | each {|ln| print $" ($ln)"}
|
||||
} else {
|
||||
$srv_rows
|
||||
| select name state
|
||||
| update state {|r| state-color $r.state}
|
||||
| rename --column {state: deploy}
|
||||
| table | str trim | lines | each {|ln| print $" ($ln)"}
|
||||
}
|
||||
}
|
||||
print ""
|
||||
}
|
||||
if not $is_live {
|
||||
let ts = (comp-cache-last-sync $ws_root_l $infra_l)
|
||||
if ($ts | is-not-empty) {
|
||||
print $"(_ansi dark_gray)from cache updated ($ts)(_ansi reset)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"status" | "st" | "s" => {
|
||||
if ($name | is-empty) {
|
||||
print "Error: component status requires a name"
|
||||
print "Usage: prvng component status <name> [--workspace <ws>]"
|
||||
return
|
||||
}
|
||||
let ws_root_s = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
|
||||
let infra_s = (comp-ws-infra $ws_root_s)
|
||||
let info = (comp-load-component-record $name $ws_root_s $infra_s)
|
||||
let dag = ($info.dag? | default [])
|
||||
let overall = if ($dag | is-empty) {
|
||||
"— (no DAG)"
|
||||
} else if ($dag | all { $in.state == "completed" }) {
|
||||
"✅ deployed"
|
||||
} else if ($dag | any { $in.state == "running" }) {
|
||||
"🔄 running"
|
||||
} else if ($dag | any { $in.state == "failed" }) {
|
||||
"❌ failed"
|
||||
} else if ($dag | any { $in.state == "blocked" }) {
|
||||
"⊘ blocked"
|
||||
} else if ($dag | any { $in.state == "unknown" }) {
|
||||
"? unknown (reset/reprovisioned)"
|
||||
} else {
|
||||
"⏳ pending"
|
||||
}
|
||||
print $"component: ($name)"
|
||||
print $"state: ($overall)"
|
||||
print $"mode: ($info.mode)"
|
||||
print $"target: (if ($info.target | is-not-empty) { $info.target } else { '—' })"
|
||||
print $"namespace: (if ($info.namespace | is-not-empty) { $info.namespace } else { '—' })"
|
||||
print $"version: (if ($info.version | is-not-empty) { $info.version } else { '—' })"
|
||||
let req_count = ($info.requires | length)
|
||||
let ops_count = ($info.operations | length)
|
||||
if $req_count > 0 { print $"requires: ($info.requires | str join ', ')" }
|
||||
if $ops_count > 0 { print $"operations: ($ops_count) defined" }
|
||||
if $live {
|
||||
print ""
|
||||
let dag_server = ($dag | get 0? | default null | get -o server | default "")
|
||||
let k8s_data = (comp-ssh-k8s-data $ws_root_s $infra_s $dag_server)
|
||||
let check = (comp-live-check $name $info $k8s_data)
|
||||
let live_icon = match $check.live {
|
||||
"healthy" => "✅",
|
||||
"degraded" => "❌",
|
||||
"starting" => "🔄",
|
||||
"partial" => "⚠",
|
||||
"not deployed" => "⊘",
|
||||
_ => "—",
|
||||
}
|
||||
let live_label = if $check.live == "—" {
|
||||
$"($live_icon) ($check.detail)"
|
||||
} else {
|
||||
$"($live_icon) ($check.live) ($check.detail)"
|
||||
}
|
||||
print $"live check: ($live_label)"
|
||||
}
|
||||
if ($dag | is-not-empty) {
|
||||
print ""
|
||||
print "DAG execution:"
|
||||
for entry in $dag {
|
||||
let state_icon = match $entry.state {
|
||||
"completed" => "✅",
|
||||
"running" => "🔄",
|
||||
"failed" => "❌",
|
||||
"blocked" => "⊘",
|
||||
"unknown" => "?",
|
||||
_ => "⏳",
|
||||
}
|
||||
let ts = if ($entry.ended_at | is-not-empty) {
|
||||
$entry.ended_at | str replace "T" " " | str replace "Z" ""
|
||||
} else if ($entry.started_at | is-not-empty) {
|
||||
$"started ($entry.started_at | str replace 'T' ' ' | str replace 'Z' '')"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
let blocker_suffix = if ($entry.blocker | is-not-empty) {
|
||||
$" ← blocked by ($entry.blocker)"
|
||||
} else { "" }
|
||||
print $" ($state_icon) ($entry.formula | fill -a l -w 24) server: ($entry.server) ($entry.state | fill -a l -w 10) ($ts)($blocker_suffix)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"show" | "info" | "sh" => {
|
||||
if ($name | is-empty) {
|
||||
print "Error: component show requires a name"
|
||||
print "Usage: prvng component show <name> [--workspace <ws>]"
|
||||
return
|
||||
}
|
||||
if ($ws | is-not-empty) {
|
||||
main component info $name --workspace $ws
|
||||
} else {
|
||||
main component info $name
|
||||
}
|
||||
}
|
||||
"install" => {
|
||||
if ($name | is-empty) { print "Error: component install requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component install $name --workspace $ws --server $server --check=$effective_check --wait=$wait
|
||||
} else {
|
||||
main component install $name --server $server --check=$effective_check --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"delete" => {
|
||||
if ($name | is-empty) { print "Error: component delete requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component delete $name --workspace $ws --server $server --check=$effective_check --wait=$wait
|
||||
} else {
|
||||
main component delete $name --server $server --check=$effective_check --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"update" => {
|
||||
if ($name | is-empty) { print "Error: component update requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component update $name --workspace $ws --server $server --check=$effective_check --wait=$wait
|
||||
} else {
|
||||
main component update $name --server $server --check=$effective_check --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"reinstall" => {
|
||||
if ($name | is-empty) { print "Error: component reinstall requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component reinstall $name --workspace $ws --server $server --check=$effective_check --wait=$wait
|
||||
} else {
|
||||
main component reinstall $name --server $server --check=$effective_check --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"restart" => {
|
||||
if ($name | is-empty) { print "Error: component restart requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component restart $name --workspace $ws --server $server --wait=$wait
|
||||
} else {
|
||||
main component restart $name --server $server --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"backup" => {
|
||||
if ($name | is-empty) { print "Error: component backup requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component backup $name --workspace $ws --server $server --wait=$wait
|
||||
} else {
|
||||
main component backup $name --server $server --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"restore" => {
|
||||
if ($name | is-empty) { print "Error: component restore requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component restore $name --workspace $ws --server $server --check=$effective_check --wait=$wait
|
||||
} else {
|
||||
main component restore $name --server $server --check=$effective_check --wait=$wait
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"check-updates" => {
|
||||
if ($name | is-empty) { print "Error: component check-updates requires a name"; return }
|
||||
let task_ids = if ($ws | is-not-empty) {
|
||||
main component check-updates $name --workspace $ws --server $server
|
||||
} else {
|
||||
main component check-updates $name --server $server
|
||||
}
|
||||
$task_ids | each {|id| print $"task: ($id)"}
|
||||
}
|
||||
"mark-complete" | "mc" => {
|
||||
if ($name | is-empty) { print "Error: component mark-complete requires a name"; return }
|
||||
if ($ws | is-not-empty) {
|
||||
main component mark-complete $name --workspace $ws --server $server
|
||||
} else {
|
||||
main component mark-complete $name --server $server
|
||||
}
|
||||
}
|
||||
"mark-complete-live" | "mcl" => {
|
||||
let ws_root = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
|
||||
let infra = (comp-ws-infra $ws_root)
|
||||
let state_root = if (($ws_root | path join "infra" $infra ".provisioning-state.ncl") | path exists) {
|
||||
$ws_root | path join "infra" $infra
|
||||
} else { $ws_root }
|
||||
let rows = with-env { PROVISIONING_NO_CACHE: "true" } {
|
||||
if ($ws | is-not-empty) {
|
||||
main component list --workspace $ws --mode $mode
|
||||
} else {
|
||||
main component list --mode $mode
|
||||
}
|
||||
}
|
||||
let ssh_server = (comp-state-server $state_root)
|
||||
let k8s_data = (comp-ssh-k8s-data $ws_root $infra $ssh_server)
|
||||
let candidates = (
|
||||
$rows
|
||||
| where { $in.state in ["running", "failed"] }
|
||||
| each {|row|
|
||||
let check = (comp-live-check $row.name $row $k8s_data)
|
||||
if $check.live == "healthy" {
|
||||
[{ name: $row.name, server: ($row.server | default ""), prior_state: $row.state }]
|
||||
} else { [] }
|
||||
}
|
||||
| flatten
|
||||
)
|
||||
if ($candidates | is-empty) {
|
||||
print "No components with state=running/failed and live=healthy found."
|
||||
return
|
||||
}
|
||||
print $"Marking ($candidates | length) components complete:"
|
||||
for c in $candidates {
|
||||
print $" → ($c.name) [($c.prior_state) → completed]"
|
||||
let srv = if ($c.server | is-not-empty) { $c.server } else { $server }
|
||||
try {
|
||||
if ($ws | is-not-empty) {
|
||||
main component mark-complete $c.name --workspace $ws --server $srv
|
||||
} else {
|
||||
main component mark-complete $c.name --server $srv
|
||||
}
|
||||
} catch {|e|
|
||||
print $" (_ansi yellow)skipped: ($e.msg)(_ansi reset)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"mark-complete-all" | "mca" => {
|
||||
let ws_root = if ($ws | is-not-empty) { comp-ws-path $ws } else { comp-ws-path "" }
|
||||
let infra = (comp-ws-infra $ws_root)
|
||||
let state_root = if (($ws_root | path join "infra" $infra ".provisioning-state.ncl") | path exists) {
|
||||
$ws_root | path join "infra" $infra
|
||||
} else { $ws_root }
|
||||
let rows = with-env { PROVISIONING_NO_CACHE: "true" } {
|
||||
if ($ws | is-not-empty) {
|
||||
main component list --workspace $ws --mode $mode
|
||||
} else {
|
||||
main component list --mode $mode
|
||||
}
|
||||
}
|
||||
let ssh_server = (comp-state-server $state_root)
|
||||
let candidates = (
|
||||
$rows
|
||||
| where { $in.state == "running" }
|
||||
| each {|row|
|
||||
[{ name: $row.name, server: ($row.server | default "") }]
|
||||
}
|
||||
| flatten
|
||||
)
|
||||
if ($candidates | is-empty) {
|
||||
print "No components with state=running found."
|
||||
return
|
||||
}
|
||||
print $"Marking ($candidates | length) components complete \(no live check\):"
|
||||
for c in $candidates {
|
||||
print $" → ($c.name)"
|
||||
let srv = if ($c.server | is-not-empty) { $c.server } else { $ssh_server }
|
||||
try {
|
||||
if ($ws | is-not-empty) {
|
||||
main component mark-complete $c.name --workspace $ws --server $srv
|
||||
} else {
|
||||
main component mark-complete $c.name --server $srv
|
||||
}
|
||||
} catch {|e|
|
||||
print $" (_ansi yellow)skipped: ($e.msg)(_ansi reset)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"bulk-mark-complete" | "bmc" => {
|
||||
let include_failed = ($rest | any { $in == "--include-failed" })
|
||||
let is_dry = ($dry_run or $check or ($rest | any { $in == "--dry-run" }))
|
||||
if $is_dry {
|
||||
if ($ws | is-not-empty) {
|
||||
main component bulk-mark-complete --workspace $ws --infra $infra --dry-run
|
||||
} else {
|
||||
main component bulk-mark-complete --infra $infra --dry-run
|
||||
}
|
||||
} else if $include_failed {
|
||||
if ($ws | is-not-empty) {
|
||||
main component bulk-mark-complete --workspace $ws --infra $infra --include-failed
|
||||
} else {
|
||||
main component bulk-mark-complete --infra $infra --include-failed
|
||||
}
|
||||
} else {
|
||||
if ($ws | is-not-empty) {
|
||||
main component bulk-mark-complete --workspace $ws --infra $infra
|
||||
} else {
|
||||
main component bulk-mark-complete --infra $infra
|
||||
}
|
||||
}
|
||||
}
|
||||
"state-check" | "sc" => {
|
||||
if ($ws | is-not-empty) {
|
||||
main component state-check --workspace $ws --infra $infra --server $server
|
||||
} else {
|
||||
main component state-check --infra $infra --server $server
|
||||
}
|
||||
}
|
||||
"state-normalize" | "sn" => {
|
||||
let is_dry = ($dry_run or $check)
|
||||
if $is_dry {
|
||||
if ($ws | is-not-empty) {
|
||||
main component state-normalize --workspace $ws --infra $infra --dry-run
|
||||
} else {
|
||||
main component state-normalize --infra $infra --dry-run
|
||||
}
|
||||
} else {
|
||||
if ($ws | is-not-empty) {
|
||||
main component state-normalize --workspace $ws --infra $infra
|
||||
} else {
|
||||
main component state-normalize --infra $infra
|
||||
}
|
||||
}
|
||||
}
|
||||
"drop-server" | "ds" => {
|
||||
if ($name | is-empty) { print "Error: drop-server requires a hostname"; return }
|
||||
let is_dry = ($dry_run or $check)
|
||||
if $is_dry {
|
||||
if ($ws | is-not-empty) {
|
||||
main component drop-server $name --workspace $ws --infra $infra --dry-run
|
||||
} else {
|
||||
main component drop-server $name --infra $infra --dry-run
|
||||
}
|
||||
} else {
|
||||
if ($ws | is-not-empty) {
|
||||
main component drop-server $name --workspace $ws --infra $infra
|
||||
} else {
|
||||
main component drop-server $name --infra $infra
|
||||
}
|
||||
}
|
||||
}
|
||||
"drop-key" | "dk" => {
|
||||
if ($name | is-empty) { print "Error: drop-key requires a key name"; return }
|
||||
if ($server | is-empty) { print "Error: drop-key requires --server <hostname>"; return }
|
||||
let is_dry = ($dry_run or $check)
|
||||
if $is_dry {
|
||||
if ($ws | is-not-empty) {
|
||||
main component drop-key $name --server $server --workspace $ws --infra $infra --dry-run
|
||||
} else {
|
||||
main component drop-key $name --server $server --infra $infra --dry-run
|
||||
}
|
||||
} else {
|
||||
if ($ws | is-not-empty) {
|
||||
main component drop-key $name --server $server --workspace $ws --infra $infra
|
||||
} else {
|
||||
main component drop-key $name --server $server --infra $infra
|
||||
}
|
||||
}
|
||||
}
|
||||
"render" => {
|
||||
if ($name | is-empty) { print "Error: component render requires a name"; return }
|
||||
if $bundle {
|
||||
main component render $name --op $op --workspace $ws --infra $infra --out $out --bundle
|
||||
} else {
|
||||
main component render $name --op $op --workspace $ws --infra $infra --out $out
|
||||
}
|
||||
}
|
||||
"help" | "h" | "-h" | "--help" => { do $help_text }
|
||||
_ => {
|
||||
print $"Unknown component subcommand: ($sub)"
|
||||
print "Run: prvng component help"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
3611
nulib/cli/components.nu
Normal file
3611
nulib/cli/components.nu
Normal file
File diff suppressed because it is too large
Load diff
48
nulib/cli/config.nu
Normal file
48
nulib/cli/config.nu
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for validate | env | show | config commands.
|
||||
# Loads only commands/configuration.nu.
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use cli/handlers/configuration.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--out: string = ""
|
||||
--debug (-x)
|
||||
--yes (-y)
|
||||
--check (-c)
|
||||
--notitles
|
||||
--verbose
|
||||
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let cmd = ($args | get 0? | default "")
|
||||
let ops = ($args | skip 1 | str join " ")
|
||||
let flags = (parse_common_flags {
|
||||
debug: $debug, out: ($out | default ""), notitles: $notitles,
|
||||
infra: ($infra | default ""), yes: $yes, check: $check, verbose: $verbose
|
||||
})
|
||||
handle_configuration_command $cmd $ops $flags
|
||||
}
|
||||
207
nulib/cli/contexts.nu
Normal file
207
nulib/cli/contexts.nu
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
|
||||
use orchestration/ops.nu [provisioning_context_options]
|
||||
use platform/config/accessor.nu *
|
||||
use platform/setup/mod.nu *
|
||||
|
||||
# Manage contexts settings
|
||||
export def "main context" [
|
||||
task?: string # server (s) | task (t) | service (sv)
|
||||
name?: string # server (s) | task (t) | service (sv)
|
||||
--key (-k): string
|
||||
--value (-v): string
|
||||
...args # Args for create command
|
||||
--reset (-r) # Restore defaults
|
||||
--serverpos (-p): int # Server position in settings
|
||||
--wait (-w) # Wait servers to be created
|
||||
--settings (-s): string # Settings path
|
||||
--outfile (-o): string # Output file
|
||||
--debug (-x) # Use Debug mode
|
||||
--xm # Debug with PROVISIONING_METADATA
|
||||
--xc # Debuc 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
|
||||
--metadata # Error with metadata (-xm)
|
||||
--notitles # not tittles
|
||||
] {
|
||||
parse_help_command "context" --task {provisioning_context_options} --end
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let config_path = (setup_config_path)
|
||||
let default_context_path = ($config_path | path join "default_context.yaml")
|
||||
let name_context_path = ($config_path | path join $"($name).yaml")
|
||||
let context_path = ($config_path | path join "context.yaml")
|
||||
let set_as_default = {
|
||||
rm -f $context_path
|
||||
^ln -s $name_context_path $context_path
|
||||
_print (
|
||||
$"(_ansi blue_bold)($name)(_ansi reset) set as (_ansi green)default context(_ansi reset)" +
|
||||
$" in (_ansi default_dimmed)($config_path)(_ansi reset)"
|
||||
)
|
||||
}
|
||||
match $task {
|
||||
"h" => {
|
||||
^$"((get-provisioning-name))" context --help
|
||||
_print (provisioning_context_options)
|
||||
}
|
||||
"create" | "c" | "new" => {
|
||||
if $name == null or $name == "" {
|
||||
_print $"🛑 No (_ansi red)name(_ansi reset) value "
|
||||
}
|
||||
if ($name_context_path |path exists) {
|
||||
_print $"(_ansi blue_bold)($name)(_ansi reset) already in (_ansi default_dimmed)($config_path)(_ansi reset)"
|
||||
} else {
|
||||
^cp $default_context_path $name_context_path
|
||||
open -r $name_context_path | str replace "infra: " $"infra: ($name)" | save -f $name_context_path
|
||||
_print $"(_ansi blue_bold)($name)(_ansi reset) created in (_ansi default_dimmed)($config_path)(_ansi reset)"
|
||||
}
|
||||
do $set_as_default
|
||||
},
|
||||
"default" | "d" => {
|
||||
if $name == null or $name == "" {
|
||||
_print $"🛑 No (_ansi red)name(_ansi reset) value "
|
||||
exit 1
|
||||
}
|
||||
if not ($name_context_path | path exists) {
|
||||
_print $"🛑 No (_ansi red)($name)(_ansi reset) found in (_ansi default_dimmed)($config_path)(_ansi reset) "
|
||||
exit 1
|
||||
}
|
||||
do $set_as_default
|
||||
},
|
||||
"remove" | "r" => {
|
||||
if $name == null {
|
||||
_print $"🛑 No (_ansi red)name(_ansi reset) value "
|
||||
exit 1
|
||||
}
|
||||
if $name == "" or not ( $name_context_path | path exists) {
|
||||
_print $"🛑 context path (_ansi blue_bold)($name)(_ansi reset) not found "
|
||||
exit 1
|
||||
}
|
||||
let context = (setup_user_context $name)
|
||||
let curr_infra = ($context | get infra? | default null)
|
||||
if $curr_infra == $name {
|
||||
_print (
|
||||
$"(_ansi blue_bold)($name)(_ansi reset) removed as (_ansi green)default context(_ansi reset) " +
|
||||
$" in (_ansi default_dimmed)($config_path)(_ansi reset)"
|
||||
)
|
||||
}
|
||||
rm -f $name_context_path $context_path
|
||||
_print $"(_ansi blue_bold)($name)(_ansi reset) context removed "
|
||||
},
|
||||
"edit" | "e" => {
|
||||
let editor = ($env | get EDITOR? | default "vi")
|
||||
let config_path = (setup_user_context_path $name)
|
||||
^$editor $config_path
|
||||
},
|
||||
"view" | "v" => {
|
||||
_print ((setup_user_context $name) | table -e)
|
||||
},
|
||||
"set" | "s" => {
|
||||
let context = (setup_user_context $name)
|
||||
let curr_value = if ($key in ($context | columns)) { $context | get $key } else { null }
|
||||
if $curr_value == null {
|
||||
_print $"🛑 invalid ($key) in setup "
|
||||
exit 1
|
||||
}
|
||||
if $curr_value == $value {
|
||||
_print $"🛑 ($key) ($value) already set "
|
||||
exit 1
|
||||
}
|
||||
# if $context != null and ( $context.infra | path exists) { return $context.infra }
|
||||
let new_context = ($context | update $key $value)
|
||||
setup_save_context $new_context
|
||||
},
|
||||
"i" | "install" => {
|
||||
install_config (if $reset { "reset" } else { "" }) --context
|
||||
},
|
||||
_ => {
|
||||
invalid_task "context" ($task | default "") --end
|
||||
},
|
||||
}
|
||||
end_run $" create ($task) "
|
||||
}
|
||||
|
||||
# Create workspace context file
|
||||
export def "create-workspace-context" [
|
||||
workspace_name: string
|
||||
workspace_path: string
|
||||
--set-active = true
|
||||
] {
|
||||
let user_config_dir = (setup_config_path)
|
||||
let context_file = ($user_config_dir | path join $"ws_($workspace_name).yaml")
|
||||
|
||||
# Load template and interpolate
|
||||
let template_path = ([
|
||||
(get-provisioning-base-path)
|
||||
"provisioning" "config" "templates" "user-context.yaml.template"
|
||||
] | path join)
|
||||
|
||||
if not ($template_path | path exists) {
|
||||
error make {
|
||||
msg: $"Template not found: ($template_path)"
|
||||
}
|
||||
}
|
||||
|
||||
let template = (open $template_path)
|
||||
let content = (
|
||||
$template
|
||||
| str replace --all "{{workspace.name}}" $workspace_name
|
||||
| str replace --all "{{workspace.path}}" $workspace_path
|
||||
| str replace --all "{{now.iso}}" (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
)
|
||||
|
||||
$content | save $context_file
|
||||
|
||||
if $set_active {
|
||||
set-workspace-active $workspace_name
|
||||
}
|
||||
|
||||
print $"✅ Created workspace context: ($context_file)"
|
||||
}
|
||||
|
||||
# Set workspace as active
|
||||
export def "set-workspace-active" [
|
||||
workspace_name: string
|
||||
] {
|
||||
let user_config_dir = (setup_config_path)
|
||||
|
||||
# Deactivate all workspaces
|
||||
for file in (ls $"($user_config_dir)/ws_*.yaml" | get name) {
|
||||
let config = (open $file | from yaml)
|
||||
let updated = ($config | upsert workspace.active false)
|
||||
$updated | to yaml | save --force $file
|
||||
}
|
||||
|
||||
# Activate target workspace
|
||||
let target_file = ($user_config_dir | path join $"ws_($workspace_name).yaml")
|
||||
if ($target_file | path exists) {
|
||||
let config = (open $target_file | from yaml)
|
||||
let updated = ($config | upsert workspace.active true)
|
||||
$updated | to yaml | save --force $target_file
|
||||
print $"✅ Set ($workspace_name) as active workspace"
|
||||
} else {
|
||||
error make {
|
||||
msg: $"Workspace context not found: ($target_file)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List all workspace contexts
|
||||
export def "list-workspace-contexts" [] {
|
||||
let user_config_dir = (setup_config_path)
|
||||
let ws_files = (do { ls $"($user_config_dir)/ws_*.yaml" } | default [])
|
||||
|
||||
$ws_files | each {|file|
|
||||
let config = (open $file.name | from yaml)
|
||||
{
|
||||
name: $config.workspace.name
|
||||
path: $config.workspace.path
|
||||
active: ($config.workspace.active | default false)
|
||||
last_used: ($config.metadata.last_used | default "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get active workspace
|
||||
export def "get-active-workspace-context" [] {
|
||||
let contexts = (list-workspace-contexts)
|
||||
$contexts | where active == true | first
|
||||
}
|
||||
19
nulib/cli/control_center.nu
Normal file
19
nulib/cli/control_center.nu
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use platform/config/accessor.nu *
|
||||
|
||||
# Control Center - Policy engine with web UI
|
||||
export def "main control-center" [
|
||||
...args # Control-center command arguments
|
||||
--infra (-i): string # Infra path
|
||||
--check (-c) # Check mode only
|
||||
--out: string # Output format: json, yaml, text
|
||||
--debug (-x) # Debug mode
|
||||
] {
|
||||
# Forward to run_module system via main router
|
||||
let cmd_args = ([$args] | flatten | str join " ")
|
||||
let infra_flag = if ($infra | is-not-empty) { $"--infra ($infra)" } else { "" }
|
||||
let check_flag = if $check { "--check" } else { "" }
|
||||
let out_flag = if ($out | is-not-empty) { $"--out ($out)" } else { "" }
|
||||
let debug_flag = if $debug { "--debug" } else { "" }
|
||||
|
||||
^($env.PROVISIONING_NAME) "control-center" $cmd_args $infra_flag $check_flag $out_flag $debug_flag --notitles
|
||||
}
|
||||
89
nulib/cli/create.nu
Normal file
89
nulib/cli/create.nu
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# REMOVED: use lib_provisioning * - causes circular import (already loaded by main provisioning script)
|
||||
use orchestration/taskservs/utils.nu *
|
||||
use orchestration/taskservs/handlers.nu *
|
||||
use domain/ssh/lib.nu *
|
||||
use platform/config/accessor.nu *
|
||||
# Provider middleware now available through lib_provisioning
|
||||
|
||||
# > TaskServs create
|
||||
export def "main create" [
|
||||
task_name?: string # task in settings
|
||||
server?: string # Server hostname in settings
|
||||
...args # Args for create command
|
||||
--infra (-i): string # Infra directory
|
||||
--settings (-s): string # Settings path
|
||||
--iptype: string = "public" # Ip type to connect
|
||||
--outfile (-o): string # Output file
|
||||
--taskserv_pos (-p): int # Server position in settings
|
||||
--check (-c) # Only check mode no taskservs will be created
|
||||
--wait (-w) # Wait taskservs to be created
|
||||
--select: string # Select with task as option
|
||||
--debug (-x) # Use Debug mode
|
||||
--xm # Debug with PROVISIONING_METADATA
|
||||
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
|
||||
--xr # Debug for remote taskservs PROVISIONING_DEBUG_REMOTE
|
||||
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
|
||||
--metadata # Error with metadata (-xm)
|
||||
--notitles # not tittles
|
||||
--helpinfo (-h) # For more details use options "help" (no dashes)
|
||||
--out: string # Print Output format: json, yaml, text (default)
|
||||
] {
|
||||
if ($out | is-not-empty) {
|
||||
set-provisioning-out $out
|
||||
set-provisioning-no-terminal true
|
||||
}
|
||||
provisioning_init $helpinfo "taskserv create" ([($task_name | default "") ($server | default "")] | append $args)
|
||||
if $debug { set-debug-enabled true }
|
||||
if $metadata { set-metadata-enabled true }
|
||||
let curr_settings = (find_get_settings --infra $infra --settings $settings)
|
||||
let args_result = (do { (get-provisioning-args) | split row " " | get 0 } | complete)
|
||||
let task = if $args_result.exit_code == 0 { $args_result.stdout } else { null }
|
||||
let options = if ($args | length) > 0 {
|
||||
$args
|
||||
} else {
|
||||
let str_task = ((get-provisioning-args) | str replace $"($task) " "" |
|
||||
str replace $"($task_name) " "" | str replace $"($server) " "")
|
||||
let st_result = (do { $str_task | split row "-" | get 0 } | complete)
|
||||
let str_task_result = if $st_result.exit_code == 0 { $st_result.stdout } else { "" }
|
||||
($str_task_result | str trim)
|
||||
}
|
||||
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
|
||||
let ops = $"((get-provisioning-args)) " | str replace $"($task_name) " "" | str trim
|
||||
let run_create = {
|
||||
let curr_settings = (settings_with_env $curr_settings)
|
||||
set-wk-cnprov $curr_settings.wk_path
|
||||
let arr_task = if $task_name == null or $task_name == "" or $task_name == "-" { [] } else { $task_name | split row "/" }
|
||||
let match_task = if ($arr_task | length) == 0 {
|
||||
""
|
||||
} else {
|
||||
let mt_result = (do { $arr_task | get 0 } | complete)
|
||||
if $mt_result.exit_code == 0 { $mt_result.stdout } else { null }
|
||||
}
|
||||
let match_task_profile = if ($arr_task | length) < 2 {
|
||||
""
|
||||
} else {
|
||||
let mtp_result = (do { $arr_task | get 1 } | complete)
|
||||
if $mtp_result.exit_code == 0 { $mtp_result.stdout } else { null }
|
||||
}
|
||||
let match_server = if $server == null or $server == "" { "" } else { $server}
|
||||
on_taskservs $curr_settings $match_task $match_task_profile $match_server $iptype $check
|
||||
}
|
||||
match $task {
|
||||
"" if $task_name == "h" => {
|
||||
^$"((get-provisioning-name))" -mod taskserv update help --notitles
|
||||
},
|
||||
"" if $task_name == "help" => {
|
||||
^$"((get-provisioning-name))" -mod taskserv update --help
|
||||
_print (provisioning_options "update")
|
||||
},
|
||||
"c" | "create" | "" => {
|
||||
let result = desktop_run_notify $"((get-provisioning-name)) taskservs create" "-> " $run_create --timeout 11sec
|
||||
},
|
||||
_ => {
|
||||
if $task_name != "" {_print $"🛑 invalid_option ($task_name)" }
|
||||
_print $"\nUse (_ansi blue_bold)((get-provisioning-name)) -h(_ansi reset) for help on commands and options"
|
||||
}
|
||||
}
|
||||
# "" | "create"
|
||||
#if not $env.PROVISIONING_DEBUG { end_run "" }
|
||||
}
|
||||
231
nulib/cli/dag.nu
Normal file
231
nulib/cli/dag.nu
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
use domain/workspace/mod.nu *
|
||||
use platform/workspace/notation.nu *
|
||||
use tools/nickel/process.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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
158
nulib/cli/dashboard.nu
Normal file
158
nulib/cli/dashboard.nu
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# Dashboard Management Commands
|
||||
# Interactive dashboards and data visualization
|
||||
|
||||
|
||||
# Main dashboard command
|
||||
export def main [
|
||||
subcommand?: string
|
||||
...args: string
|
||||
] {
|
||||
|
||||
if ($subcommand | is-empty) {
|
||||
print "📊 Systems Provisioning Dashboard"
|
||||
print ""
|
||||
print "Interactive dashboards for infrastructure monitoring and analytics"
|
||||
print ""
|
||||
print "Usage: provisioning dashboard <subcommand> [args...]"
|
||||
print ""
|
||||
print "Subcommands:"
|
||||
print " create [template] [name] - Create interactive dashboard"
|
||||
print " start <name> [port] - Start dashboard server"
|
||||
print " list - List available dashboards"
|
||||
print " export <name> [output] - Export dashboard to HTML"
|
||||
print " demo - Create and start demo dashboard"
|
||||
print " status - Show dashboard system status"
|
||||
print ""
|
||||
print "Templates:"
|
||||
print " monitoring - Real-time logs and metrics"
|
||||
print " infrastructure - Server and cluster overview"
|
||||
print " full - Complete observability dashboard"
|
||||
print " ai-insights - AI-powered analytics and predictions"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning dashboard demo"
|
||||
print " provisioning dashboard create monitoring my-dashboard"
|
||||
print " provisioning dashboard start my-dashboard 8080"
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
match $subcommand {
|
||||
"create" => {
|
||||
marimo_integration create ...$args
|
||||
}
|
||||
"start" => {
|
||||
marimo_integration start ...$args
|
||||
}
|
||||
"list" => {
|
||||
marimo_integration list
|
||||
}
|
||||
"export" => {
|
||||
marimo_integration export ...$args
|
||||
}
|
||||
"demo" => {
|
||||
create_demo_dashboard
|
||||
}
|
||||
"status" => {
|
||||
show_dashboard_status
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown subcommand: ($subcommand)"
|
||||
print "Run 'provisioning dashboard' for help"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Create and start a demo dashboard
|
||||
def create_demo_dashboard [] {
|
||||
print "🚀 Creating demo dashboard with live data..."
|
||||
|
||||
# Check if API server is running
|
||||
let api_status = check_api_server_status
|
||||
|
||||
if not $api_status {
|
||||
print "⚠️ API server not running. Starting API server..."
|
||||
start_api_server --port 3000 --background
|
||||
sleep 3sec
|
||||
}
|
||||
|
||||
# Create AI insights dashboard
|
||||
marimo_integration create ai-insights demo-dashboard
|
||||
|
||||
print ""
|
||||
print "🎉 Demo dashboard created and started!"
|
||||
print "📊 Open your browser to: http://localhost:8080"
|
||||
print ""
|
||||
print "Features available:"
|
||||
print " 🔍 Real-time log analysis"
|
||||
print " 📈 System metrics visualization"
|
||||
print " 🏗️ Infrastructure topology"
|
||||
print " 🤖 AI-powered insights and predictions"
|
||||
print " 📊 Interactive charts and tables"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Check API server status
|
||||
def check_api_server_status [] {
|
||||
let response = (try {
|
||||
http get --allow-errors --full "http://localhost:3000/health"
|
||||
} catch {
|
||||
return false
|
||||
})
|
||||
($response.status == 200) and ($response.body | get -o status | default "" | str trim) == "healthy"
|
||||
}
|
||||
|
||||
# Start API server in background
|
||||
def start_api_server [--port: int = 3000, --background = false] {
|
||||
if $background {
|
||||
# BROKEN: file not found — use ../api/server.nu *
|
||||
nu -c "echo 'api server not available'" &
|
||||
} else {
|
||||
# BROKEN: file not found — use ../api/server.nu *
|
||||
print "API server module not available"
|
||||
}
|
||||
}
|
||||
|
||||
# Show dashboard system status
|
||||
def show_dashboard_status [] {
|
||||
print "📊 Dashboard System Status"
|
||||
print ""
|
||||
|
||||
# Check Marimo installation
|
||||
let marimo_available = check_marimo_available
|
||||
let marimo_status = if $marimo_available { "✅ Installed" } else { "❌ Not installed" }
|
||||
print $"Marimo: ($marimo_status)"
|
||||
|
||||
# Check API server
|
||||
let api_status = check_api_server_status
|
||||
let api_display = if $api_status { "✅ Running" } else { "❌ Stopped" }
|
||||
print $"API Server: ($api_display)"
|
||||
|
||||
# List dashboards
|
||||
let dashboards = list_dashboards
|
||||
print $"Dashboards: ($dashboards | length) available"
|
||||
|
||||
if ($dashboards | length) > 0 {
|
||||
print ""
|
||||
print "Available dashboards:"
|
||||
$dashboards | select name modified | table
|
||||
}
|
||||
|
||||
# System resources
|
||||
print ""
|
||||
print "System Resources:"
|
||||
let mem_info = (sys mem)
|
||||
print $"Memory: ($mem_info.used | into string) / ($mem_info.total | into string) used"
|
||||
|
||||
print ""
|
||||
print "Quick start:"
|
||||
if not $marimo_available {
|
||||
print "1. Install Marimo: provisioning dashboard create install"
|
||||
}
|
||||
if not $api_status {
|
||||
print "2. Start API: provisioning api start"
|
||||
}
|
||||
print "3. Create dashboard: provisioning dashboard demo"
|
||||
}
|
||||
112
nulib/cli/delete.nu
Normal file
112
nulib/cli/delete.nu
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
|
||||
use platform/config/accessor.nu *
|
||||
use primitives/io/help.nu [parse_help_command]
|
||||
use primitives/io/logging.nu [is-debug-enabled]
|
||||
use primitives/io/interface.nu [_print _ansi]
|
||||
use primitives/io/undefined.nu [invalid_task]
|
||||
use primitives/infra/init.nu [get-provisioning-name]
|
||||
use ../../../catalog/providers/hetzner/nulib/hetzner/api.nu *
|
||||
|
||||
def prompt_delete [
|
||||
target: string
|
||||
target_name: string
|
||||
yes: bool
|
||||
name?: string
|
||||
] {
|
||||
match $name {
|
||||
"h" | "help" => {
|
||||
^((get-provisioning-name)) "-mod" $target "--help"
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
if not $yes or not ((($env.PROVISIONING_ARGS? | default "")) | str contains "--yes") {
|
||||
_print ( $"To (_ansi red_bold)delete ($target_name) (_ansi reset) " +
|
||||
$" (_ansi green_bold)($name)(_ansi reset) type (_ansi green_bold)yes(_ansi reset) ? "
|
||||
)
|
||||
let user_input = (input --numchar 3)
|
||||
if $user_input != "yes" and $user_input != "YES" {
|
||||
exit 1
|
||||
}
|
||||
$name
|
||||
} else {
|
||||
$env.PROVISIONING_ARGS = ($env.PROVISIONING_ARGS? | find -v "yes")
|
||||
($name | default "" | str replace "yes" "")
|
||||
}
|
||||
}
|
||||
|
||||
# Delete infrastructure and services
|
||||
export def "main delete" [
|
||||
target?: string # server (s) | task (t) | service (sv)
|
||||
name?: string # target name in settings
|
||||
...args # Args for create command
|
||||
--serverpos (-p): int # Server position in settings
|
||||
--keepstorage # Keep storage
|
||||
--yes (-y) # confirm delete
|
||||
--wait (-w) # Wait servers to be created
|
||||
--infra (-i): string # Infra path
|
||||
--settings (-s): string # Settings path
|
||||
--outfile (-o): string # Output file
|
||||
--debug (-x) # Use Debug mode
|
||||
--xm # Debug with PROVISIONING_METADATA
|
||||
--xc # Debuc 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
|
||||
--metadata # Error with metadata (-xm)
|
||||
--notitles # not tittles
|
||||
--out: string # Print Output format: json, yaml, text (default)
|
||||
] {
|
||||
if ($out | is-not-empty) {
|
||||
$env.PROVISIONING_OUT = $out
|
||||
$env.PROVISIONING_NO_TERMINAL = true
|
||||
}
|
||||
parse_help_command "delete" --end
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let use_debug = if $debug or (is-debug-enabled) { "-x" } else { "" }
|
||||
match $target {
|
||||
"server"| "servers" | "s" => {
|
||||
prompt_delete "server" "servers" $yes $name
|
||||
^$"((get-provisioning-name))" $use_debug -mod "server" ($env.PROVISIONING_ARGS | str replace $target '') --yes --notitles
|
||||
},
|
||||
"storage" => {
|
||||
prompt_delete "server" "storage" $yes $name
|
||||
^$"((get-provisioning-name))" $use_debug -mod "server" $env.PROVISIONING_ARGS --yes --notitles
|
||||
},
|
||||
"taskserv" | "taskservs" | "t" => {
|
||||
prompt_delete "taskserv" "tasks/services" $yes $name
|
||||
^$"((get-provisioning-name))" $use_debug -mod "tasksrv" ($env.PROVISIONING_ARGS | str replace $target '') --yes --notitles
|
||||
},
|
||||
"clusters"| "clusters" | "cl" => {
|
||||
prompt_delete "cluster" "cluster" $yes $name
|
||||
^$"((get-provisioning-name))" $use_debug -mod "cluster" ($env.PROVISIONING_ARGS | str replace $target '') --yes --notitles
|
||||
},
|
||||
"fip" | "floating-ip" => {
|
||||
let fip_name = ($name | default "")
|
||||
if ($fip_name | is-empty) {
|
||||
error make { msg: "floating IP name required — usage: provisioning delete fip <name>" }
|
||||
}
|
||||
prompt_delete "floating-ip" "floating IP" $yes $fip_name
|
||||
|
||||
let fips = (hetzner_api_list_floating_ips)
|
||||
let matches = ($fips | where {|f| $f.name == $fip_name })
|
||||
if ($matches | is-empty) {
|
||||
error make { msg: $"Floating IP '($fip_name)' not found in Hetzner" }
|
||||
}
|
||||
let fip = ($matches | first)
|
||||
let fip_id = ($fip.id | into string)
|
||||
|
||||
let assigned = ($fip | get -o server | default null)
|
||||
if $assigned != null {
|
||||
print $" unassigning ($fip_name) from server ($assigned) ..."
|
||||
let _a = (hetzner_api_unassign_floating_ip $fip_id)
|
||||
}
|
||||
|
||||
print $" deleting floating IP ($fip_name) [($fip_id)] ..."
|
||||
hetzner_api_delete_floating_ip $fip_id
|
||||
print $" ✓ ($fip_name) deleted"
|
||||
},
|
||||
_ => {
|
||||
invalid_task "delete" ($target | default "") --end
|
||||
exit
|
||||
},
|
||||
}
|
||||
}
|
||||
42
nulib/cli/delete_prov.nu
Normal file
42
nulib/cli/delete_prov.nu
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for delete commands (server, taskserv, cluster).
|
||||
# Loads only cli/delete.nu (~45ms vs ~3s for cli.nu fallback).
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/delete.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--yes (-y)
|
||||
--debug (-x)
|
||||
--keepstorage
|
||||
--notitles
|
||||
--wait (-w)
|
||||
--settings (-s): string = ""
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let target = ($args | get 0? | default "")
|
||||
let name = ($args | get 1? | default "")
|
||||
main delete $target $name --infra $infra --yes=$yes --keepstorage=$keepstorage --notitles=$notitles --wait=$wait --settings $settings
|
||||
}
|
||||
48
nulib/cli/dev.nu
Normal file
48
nulib/cli/dev.nu
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for module | layer | discover commands.
|
||||
# Loads only commands/development.nu.
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use cli/handlers/development.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--out: string = ""
|
||||
--debug (-x)
|
||||
--yes (-y)
|
||||
--check (-c)
|
||||
--notitles
|
||||
--verbose
|
||||
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let cmd = ($args | get 0? | default "")
|
||||
let ops = ($args | skip 1 | str join " ")
|
||||
let flags = (parse_common_flags {
|
||||
debug: $debug, out: ($out | default ""), notitles: $notitles,
|
||||
infra: ($infra | default ""), yes: $yes, check: $check, verbose: $verbose
|
||||
})
|
||||
handle_development_command $cmd $ops $flags
|
||||
}
|
||||
479
nulib/cli/dispatcher.nu
Normal file
479
nulib/cli/dispatcher.nu
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
# Module: Command Dispatcher
|
||||
# Purpose: Main command router: dispatches CLI commands to appropriate handlers (infra, tools, workspace, etc.).
|
||||
# Dependencies: All command modules
|
||||
|
||||
# Command Dispatcher
|
||||
# Central routing logic for all provisioning commands
|
||||
|
||||
# Command module imports are lazy — loaded inside wrapper functions at dispatch time.
|
||||
# Only load lib_provisioning helpers required for routing logic in dispatch_command itself.
|
||||
#
|
||||
# ADR-025 Phase 4: narrowed from stars to selective imports. The two prior
|
||||
# imports `commands/traits.nu *` (20 exports) and `utils/command-registry.nu *`
|
||||
# (3 exports) were fully DEAD here — zero symbol uses — and have been removed.
|
||||
# `enforcement.nu` and `metadata_handler.nu` are narrowed to the single symbol
|
||||
# each that dispatcher actually calls (`check-and-enforce`, `validate-and-prepare`).
|
||||
use primitives/io/undefined.nu [invalid_task]
|
||||
use domain/workspace/enforcement.nu [check-and-enforce]
|
||||
use ./metadata_handler.nu [validate-and-prepare]
|
||||
use tools/nickel/process.nu [ncl-eval-soft, default-ncl-paths]
|
||||
|
||||
# Helper to run module commands
|
||||
def run_module [
|
||||
args: string
|
||||
module: string
|
||||
option?: string
|
||||
--exec
|
||||
] {
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
|
||||
if $exec {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
} else {
|
||||
^$"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
}
|
||||
}
|
||||
|
||||
# Lazy dispatch wrappers — each module is loaded only when its domain is actually invoked.
|
||||
def _dispatch_infrastructure [cmd: string, ops: string, flags: record] {
|
||||
use handlers/infrastructure.nu *
|
||||
handle_infrastructure_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_orchestration [cmd: string, ops: string, flags: record] {
|
||||
use handlers/orchestration.nu *
|
||||
handle_orchestration_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_development [cmd: string, ops: string, flags: record] {
|
||||
use handlers/development.nu *
|
||||
handle_development_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_workspace [cmd: string, ops: string, flags: record] {
|
||||
use handlers/workspace.nu *
|
||||
handle_workspace_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_config [cmd: string, ops: string, flags: record] {
|
||||
use handlers/configuration.nu *
|
||||
handle_configuration_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_utilities [cmd: string, ops: string, flags: record] {
|
||||
use handlers/utilities/mod.nu *
|
||||
handle_utility_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_generation [cmd: string, ops: string, flags: record] {
|
||||
use handlers/generation.nu *
|
||||
handle_generation_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_guides [cmd: string, ops: string, flags: record] {
|
||||
use handlers/guides.nu *
|
||||
handle_guide_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_authentication [cmd: string, ops: string, flags: record] {
|
||||
use handlers/authentication.nu *
|
||||
handle_authentication_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_diagnostics [cmd: string, ops: string, flags: record] {
|
||||
use handlers/diagnostics.nu *
|
||||
handle_diagnostics_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_vm [cmd: string, ops: string, flags: record] {
|
||||
use handlers/vm_domain.nu *
|
||||
handle_vm_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_platform [cmd: string, ops: string, flags: record] {
|
||||
use handlers/platform.nu *
|
||||
handle_platform_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_secretumvault [cmd: string, ops: string, flags: record] {
|
||||
use handlers/secretumvault.nu *
|
||||
handle_secretumvault_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_build [cmd: string, ops: string, flags: record] {
|
||||
use handlers/build.nu *
|
||||
handle_build_command $cmd $ops $flags
|
||||
}
|
||||
def _dispatch_state [cmd: string, ops: string, flags: record] {
|
||||
use handlers/state.nu *
|
||||
handle_state_command $cmd $ops $flags
|
||||
}
|
||||
|
||||
# Command registry with shortcuts and aliases
|
||||
# Maps short forms and aliases to their canonical command domain
|
||||
export def get_command_registry [] {
|
||||
# Read commands registry from Nickel configuration
|
||||
let registry_file = ($env.PROVISIONING | path join "core/nulib/commands-registry.ncl")
|
||||
|
||||
let prov = ($env.PROVISIONING? | default "/usr/local/provisioning")
|
||||
let registry_data = (ncl-eval-soft $registry_file (default-ncl-paths "") {})
|
||||
if ($registry_data | is-empty) or ($registry_data == {}) {
|
||||
print "Error loading command registry"
|
||||
return {}
|
||||
}
|
||||
let commands = $registry_data.commands
|
||||
|
||||
# Build registry record mapping commands and aliases to "category command" format
|
||||
let entries = (
|
||||
$commands | each {|cmd|
|
||||
let help_cat = $cmd.help_category
|
||||
let cmd_name = $cmd.command
|
||||
let cmd_value = $"($help_cat) ($cmd_name)"
|
||||
|
||||
# Create entries for command and all its aliases
|
||||
let command_entry = {($cmd_name): $cmd_value}
|
||||
let alias_entries = ($cmd.aliases | each {|alias| {($alias): $cmd_value}})
|
||||
|
||||
# Merge all entries
|
||||
[$command_entry] | append $alias_entries | reduce {|it, acc| $acc | merge $it}
|
||||
}
|
||||
| reduce {|it, acc| $acc | merge $it}
|
||||
)
|
||||
|
||||
$entries
|
||||
}
|
||||
|
||||
# Commands that require arguments are defined in commands-registry.ncl (Nickel config file)
|
||||
# Use utils/command-registry.nu module to query the registry via JSON export
|
||||
# Note: This is loaded dynamically when needed, not at dispatcher load time
|
||||
|
||||
# Main command dispatcher
|
||||
# Routes commands to appropriate domain handlers
|
||||
export def dispatch_command [
|
||||
args: list
|
||||
flags: record
|
||||
] {
|
||||
use flags.nu *
|
||||
|
||||
# Find first non-flag argument as the task
|
||||
# (flags have already been parsed by main function, but reorder_args may have moved them)
|
||||
let matches = ($args | enumerate | where {|item|
|
||||
not ($item.item | str starts-with "-") and ($item.item | is-not-empty)
|
||||
})
|
||||
|
||||
let task_result = if ($matches | length) > 0 {
|
||||
$matches | first
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
let task = if ($task_result | is-not-empty) {
|
||||
$task_result.item
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
# DEBUG
|
||||
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||
print $"DEBUG dispatcher: task = '($task)'" >&2
|
||||
}
|
||||
|
||||
let task_index = if ($task_result | is-not-empty) {
|
||||
$task_result.index
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
# Get remaining args after task
|
||||
let ops_list = if $task_index < ($args | length) {
|
||||
($args | skip ($task_index + 1))
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
let ops_str = ($ops_list | str join " ")
|
||||
|
||||
# Handle empty command
|
||||
if ($task | is-empty) {
|
||||
print "Use 'provisioning help' for available commands"
|
||||
exit
|
||||
}
|
||||
|
||||
# NOTE: Bash wrapper validates commands via command registry
|
||||
# Direct Nushell invocations will fail later with invalid_task if command is unknown
|
||||
|
||||
# Handle "provisioning help <category>" - DON'T dispatch, let main script handle it
|
||||
# The main script has "main help" function that Nushell will automatically route to
|
||||
# Using exec here creates infinite loop (calls bash wrapper → calls Nushell → calls exec → repeat)
|
||||
if $task in ["help" "h"] {
|
||||
# Don't dispatch help - it's handled by "export def main help" in provisioning script
|
||||
# Just exit dispatcher and let Nushell's built-in command routing handle it
|
||||
return
|
||||
}
|
||||
|
||||
# Intercept bi-directional help: "provisioning <cmd> help" → convert to "provisioning help <cmd>"
|
||||
# Then exit dispatcher so main script's "main help" function handles it
|
||||
let first_op = if ($ops_list | length) > 0 { ($ops_list | get 0) } else { "" }
|
||||
if $first_op in ["help" "h"] {
|
||||
# Bi-directional help detected: convert args and exit dispatcher
|
||||
# The main script will see "help <task>" and route to "main help"
|
||||
return
|
||||
}
|
||||
|
||||
# Resolve command through registry
|
||||
let registry = get_command_registry
|
||||
let resolved = if ($task in ($registry | columns)) { $registry | get $task } else { $task }
|
||||
|
||||
# Split into domain, command, and optional subcommand args
|
||||
let parts = ($resolved | split row " ")
|
||||
let domain = if ($parts | length) > 1 { ($parts | get 0) } else { "special" }
|
||||
let command = if ($parts | length) > 1 { ($parts | get 1) } else { $task }
|
||||
|
||||
# Extract any additional parts as pre-filled ops (for compound shortcuts like "dt" → "discover taskservs")
|
||||
let extra_ops = if ($parts | length) > 2 {
|
||||
($parts | skip 2 | str join " ")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
# Combine extra_ops with user-provided ops
|
||||
let final_ops = if ($extra_ops | is-not-empty) and ($ops_str | is-not-empty) {
|
||||
$"($extra_ops) ($ops_str)"
|
||||
} else if ($extra_ops | is-not-empty) {
|
||||
$extra_ops
|
||||
} else {
|
||||
$ops_str
|
||||
}
|
||||
|
||||
# Handle workspace override from flags
|
||||
let workspace_context = (extract-workspace-infra-from-flags $flags)
|
||||
|
||||
# Set temporary workspace context if specified
|
||||
if ($workspace_context.workspace | is-not-empty) {
|
||||
$env.TEMP_WORKSPACE = $workspace_context.workspace
|
||||
}
|
||||
|
||||
# Update infra flag if parsed from workspace:infra notation
|
||||
let updated_flags = if ($workspace_context.infra | is-not-empty) {
|
||||
$flags | merge { infra: $workspace_context.infra }
|
||||
} else {
|
||||
$flags
|
||||
}
|
||||
|
||||
# WORKSPACE ENFORCEMENT - Check workspace requirement before processing
|
||||
# This enforces that most commands require an active workspace
|
||||
let enforcement_allowed = (check-and-enforce $task $args)
|
||||
|
||||
if not $enforcement_allowed {
|
||||
# Enforcement failed - error already displayed by check-and-enforce
|
||||
exit 1
|
||||
}
|
||||
|
||||
# METADATA VALIDATION - Check command requirements and handle interactive forms
|
||||
# Build canonical command name for metadata lookup
|
||||
let canonical_name = if ($domain == "special") {
|
||||
$command
|
||||
} else if ($domain != "") {
|
||||
$"($domain) ($command)"
|
||||
} else {
|
||||
$command
|
||||
}
|
||||
|
||||
# Validate command and prepare execution (handles interactive forms)
|
||||
let prep_result = (validate-and-prepare $canonical_name $updated_flags)
|
||||
|
||||
if not $prep_result.proceed {
|
||||
# Validation failed - error already displayed by validate-and-prepare
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Set environment based on flags
|
||||
set_debug_env $updated_flags
|
||||
|
||||
# Ensure PROVISIONING_INFRA is explicitly set if infra flag was provided
|
||||
# This ensures context-aware filtering works with --infra flag
|
||||
let infra_flag = ($updated_flags | get --optional infra)
|
||||
if ($infra_flag | is-not-empty) {
|
||||
$env.PROVISIONING_INFRA = $infra_flag
|
||||
}
|
||||
|
||||
# Dispatch to domain handler
|
||||
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||
print $"DEBUG: Dispatching to domain='($domain)' command='($command)' final_ops='($final_ops)'" >&2
|
||||
}
|
||||
|
||||
# Handler registry - maps domain to handler closure
|
||||
# To add a new command category:
|
||||
# 1. Add to commands-registry.ncl with help_category
|
||||
# 2. Add handler closure here
|
||||
# 3. Create handle_CATEGORY_command function in commands/ module
|
||||
let handlers = {
|
||||
infrastructure: {|cmd, ops, flags| _dispatch_infrastructure $cmd $ops $flags}
|
||||
orchestration: {|cmd, ops, flags| _dispatch_orchestration $cmd $ops $flags}
|
||||
development: {|cmd, ops, flags| _dispatch_development $cmd $ops $flags}
|
||||
workspace: {|cmd, ops, flags| _dispatch_workspace $cmd $ops $flags}
|
||||
config: {|cmd, ops, flags| _dispatch_config $cmd $ops $flags}
|
||||
utils: {|cmd, ops, flags| _dispatch_utilities $cmd $ops $flags}
|
||||
generation: {|cmd, ops, flags| _dispatch_generation $cmd $ops $flags}
|
||||
guides: {|cmd, ops, flags| _dispatch_guides $cmd $ops $flags}
|
||||
authentication: {|cmd, ops, flags| _dispatch_authentication $cmd $ops $flags}
|
||||
secretumvault: {|cmd, ops, flags| _dispatch_secretumvault $cmd $ops $flags}
|
||||
diagnostics: {|cmd, ops, flags| _dispatch_diagnostics $cmd $ops $flags}
|
||||
integrations: {|cmd, ops, flags| handle_integrations_command $cmd $ops $flags}
|
||||
platform: {|cmd, ops, flags| _dispatch_platform $cmd $ops $flags}
|
||||
vm: {|cmd, ops, flags| _dispatch_vm $cmd $ops $flags}
|
||||
build: {|cmd, ops, flags| _dispatch_build $cmd $ops $flags}
|
||||
state: {|cmd, ops, flags| _dispatch_state $cmd $ops $flags}
|
||||
special: {|cmd, ops, flags| handle_special_command $cmd $ops $flags}
|
||||
test: {|cmd, ops, flags| handle_test_command $cmd $ops $flags}
|
||||
help: {|cmd, ops, flags| exec $"($env.PROVISIONING_NAME)" help $cmd --notitles}
|
||||
}
|
||||
|
||||
# Dynamic dispatch based on domain
|
||||
if ($domain in ($handlers | columns)) {
|
||||
let handler = ($handlers | get $domain)
|
||||
do $handler $command $final_ops $updated_flags
|
||||
} else {
|
||||
print $"❌ Error: No handler registered for domain '($domain)'"
|
||||
print $" Command: ($task)"
|
||||
print $" Available handlers: ($handlers | columns | str join ', ')"
|
||||
print ""
|
||||
print "To fix: Add handler closure to dispatcher.nu handlers record"
|
||||
invalid_task "" $task --end
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean up temporary workspace context
|
||||
if ($workspace_context.workspace | is-not-empty) {
|
||||
hide-env TEMP_WORKSPACE
|
||||
}
|
||||
}
|
||||
|
||||
# Integrations command handler (prov-ecosystem + provctl)
|
||||
def handle_integrations_command [command: string, ops: string, flags: record] {
|
||||
use handlers/integrations/mod.nu *
|
||||
let args_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where { |x| ($x | is-not-empty) }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
let check_mode = if $flags.check_mode { "--check" } else { "" }
|
||||
|
||||
# Parse command - could be "integrations integrations" or just the subcommand
|
||||
let subcommand = if $command == "integrations" {
|
||||
($args_list | get 0?)
|
||||
} else {
|
||||
$command
|
||||
}
|
||||
|
||||
# Get remaining args
|
||||
let remaining_args = if $command == "integrations" {
|
||||
($args_list | skip 1)
|
||||
} else {
|
||||
$args_list
|
||||
}
|
||||
|
||||
# Call the integrations handler with parsed arguments
|
||||
if ($subcommand == null) {
|
||||
cmd-integrations "help" $remaining_args --check=($check_mode | is-not-empty)
|
||||
} else {
|
||||
cmd-integrations $subcommand $remaining_args --check=($check_mode | is-not-empty)
|
||||
}
|
||||
}
|
||||
|
||||
# Test command handler
|
||||
def handle_test_command [command: string, ops: string, flags: record] {
|
||||
let args = if ($ops | is-not-empty) { $ops } else { "" }
|
||||
run_module $args "test" --exec
|
||||
}
|
||||
|
||||
# Special command handler (create, delete, update, deploy, etc.)
|
||||
def handle_special_command [command: string, ops: string, flags: record] {
|
||||
match $command {
|
||||
"create" | "c" => {
|
||||
let use_debug = if $flags.debug_mode or ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||||
let str_out = if ($flags.outfile | is-not-empty) { $"--outfile ($flags.outfile) " } else { "" }
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug "create" $ops $use_check $str_infra $str_out --notitles
|
||||
}
|
||||
|
||||
"delete" | "d" => {
|
||||
let use_debug = if $flags.debug_mode { "-x" } else { "" }
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let use_yes = if $flags.auto_confirm { "--yes " } else { "" }
|
||||
let use_keepstorage = if $flags.keep_storage { "--keepstorage " } else { "" }
|
||||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||||
exec $"($env.PROVISIONING_NAME)" "delete" $ops $use_check $use_yes $use_keepstorage $str_infra --notitles
|
||||
}
|
||||
|
||||
"update" | "u" => {
|
||||
let use_debug = if $flags.debug_mode { "-x" } else { "" }
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||||
exec $"($env.PROVISIONING_NAME)" "update" $ops $use_check $str_infra --notitles
|
||||
}
|
||||
|
||||
"price" | "prices" | "cost" | "costs" => {
|
||||
use handlers/infrastructure.nu *
|
||||
handle_price_command $ops $flags
|
||||
}
|
||||
|
||||
"create-server-task" | "cst" | "csts" | "create-servers-tasks" => {
|
||||
use handlers/infrastructure.nu *
|
||||
handle_create_server_task $ops $flags
|
||||
}
|
||||
|
||||
"new" => {
|
||||
let str_new = ($flags.new_infra | default "")
|
||||
print $"\n (_ansi yellow)New Infra ($str_new)(_ansi reset)"
|
||||
}
|
||||
|
||||
"ai" => {
|
||||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||||
let str_settings = if ($flags.settings | is-not-empty) { $"--settings ($flags.settings) " } else { "" }
|
||||
let str_out = if ($flags.output_format | is-not-empty) { $"--out ($flags.output_format) " } else { "" }
|
||||
run_module $"($ops) ($str_infra) ($str_settings) ($str_out)" "ai" --exec
|
||||
}
|
||||
|
||||
"context" | "ctx" => {
|
||||
^$"($env.PROVISIONING_NAME)" "context" $ops --notitles
|
||||
run_module $ops "" --exec
|
||||
}
|
||||
|
||||
"setup" | "st" | "config" => {
|
||||
# Route to full setup command handler
|
||||
use handlers/setup.nu *
|
||||
|
||||
let args_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where { |x| ($x | is-not-empty) }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
let command = if ($args_list | length) > 0 { ($args_list | get 0) } else { "help" }
|
||||
let remaining_args = if ($args_list | length) > 1 { ($args_list | skip 1) } else { [] }
|
||||
|
||||
cmd-setup $command $remaining_args --check=$flags.check_mode --verbose=$flags.debug_mode --yes=$flags.auto_confirm
|
||||
}
|
||||
|
||||
"control-center" => {
|
||||
run_module $ops "control-center" --exec
|
||||
}
|
||||
|
||||
"mcp-server" => {
|
||||
run_module $ops "mcp-server" --exec
|
||||
}
|
||||
|
||||
"volume" | "vol" => {
|
||||
let vol_args = if ($ops | is-not-empty) { $ops | split row " " | where { $in | is-not-empty } } else { [] }
|
||||
exec $"($env.PROVISIONING_NAME)" "volume" ...$vol_args
|
||||
}
|
||||
|
||||
"storage-box" | "store-box" | "sbox" => {
|
||||
# Self-contained nu module (cli/storage_box.nu) — invoked as a script
|
||||
# directly rather than via a domain handler, mirroring how an operator
|
||||
# would run it by hand. Its own `main` parses [plan|create|list|status|
|
||||
# delete|howto] and --infra/--workspace/--debug.
|
||||
let mod_path = ($env.PROVISIONING | path join "core/nulib/cli/storage_box.nu")
|
||||
let sb_args = if ($ops | is-not-empty) { $ops | split row " " | where { $in | is-not-empty } } else { [] }
|
||||
let infra_args = if ($flags.infra | is-not-empty) { ["--infra" $flags.infra] } else { [] }
|
||||
let ws_args = if ($flags.workspace | is-not-empty) { ["--workspace" $flags.workspace] } else { [] }
|
||||
let debug_args = if $flags.debug_mode { ["--debug"] } else { [] }
|
||||
^nu $mod_path ...$sb_args ...$infra_args ...$ws_args ...$debug_args
|
||||
}
|
||||
|
||||
_ => {
|
||||
print $"❌ Unknown command: ($command)"
|
||||
print "Use 'provisioning help' for available commands"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
19
nulib/cli/fastpath/get-help-category.nu
Executable file
19
nulib/cli/fastpath/get-help-category.nu
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env nu
|
||||
# Get help category for a command (if it requires arguments)
|
||||
# Usage: nu get-help-category.nu <schema_file> <command>
|
||||
|
||||
use tools/nickel/process.nu [ncl-eval]
|
||||
|
||||
def main [schema_file: string, cmd: string] {
|
||||
let json = (ncl-eval $schema_file [])
|
||||
let commands = $json.commands
|
||||
let result = ($commands | where { |c|
|
||||
(($c.command == $cmd) or ($c.aliases | any { |a| $a == $cmd })) and $c.requires_args
|
||||
} | first)
|
||||
|
||||
if ($result | is-not-empty) {
|
||||
$result.help_category
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
201
nulib/cli/fastpath/lib_minimal.nu
Normal file
201
nulib/cli/fastpath/lib_minimal.nu
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env nu
|
||||
# Minimal Library - Fast path for interactive commands
|
||||
# NO config loading, NO platform bootstrap
|
||||
# Follows: @.claude/guidelines/nushell/NUSHELL_GUIDELINES.md
|
||||
# Error handling: Result pattern (hybrid, no try-catch)
|
||||
|
||||
use primitives/result.nu *
|
||||
|
||||
# Get user config path (centralized location)
|
||||
# Rule 2: Single purpose function.
|
||||
# Cross-platform (macOS, Linux, Windows) + $PROVISIONING_USER_CONFIG_DIR override.
|
||||
# Intentionally local (not imported) to keep fastpath startup lean.
|
||||
def get-user-config-path [] {
|
||||
let override = ($env.PROVISIONING_USER_CONFIG_DIR? | default "")
|
||||
if ($override | is-not-empty) {
|
||||
return ($override | path join "user_config.yaml" | path expand)
|
||||
}
|
||||
|
||||
let home = ($env.HOME? | default "")
|
||||
let dir = match $nu.os-info.name {
|
||||
"macos" => ([$home "Library" "Application Support" "provisioning"] | path join)
|
||||
"linux" => {
|
||||
let xdg = ($env.XDG_CONFIG_HOME? | default "")
|
||||
if ($xdg | is-not-empty) {
|
||||
($xdg | path join "provisioning")
|
||||
} else {
|
||||
([$home ".config" "provisioning"] | path join)
|
||||
}
|
||||
}
|
||||
"windows" => {
|
||||
let appdata = ($env.APPDATA? | default "")
|
||||
if ($appdata | is-not-empty) {
|
||||
($appdata | path join "provisioning")
|
||||
} else {
|
||||
([$home "AppData" "Roaming" "provisioning"] | path join)
|
||||
}
|
||||
}
|
||||
_ => ([$home ".config" "provisioning"] | path join)
|
||||
}
|
||||
|
||||
($dir | path join "user_config.yaml" | path expand)
|
||||
}
|
||||
|
||||
# List all registered workspaces
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
# Rule 2: Single purpose - only list workspaces
|
||||
# Result: {ok: list, err: null} on success; {ok: null, err: message} on error
|
||||
export def workspace-list [] {
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Guard: Early return if config doesn't exist
|
||||
if not ($user_config | path exists) {
|
||||
return (ok [])
|
||||
}
|
||||
|
||||
# Guard: File is guaranteed to exist, open directly (no try-catch)
|
||||
let config = (open $user_config)
|
||||
|
||||
let active = ($config | get --optional active_workspace | default "")
|
||||
let workspaces = ($config | get --optional workspaces | default [])
|
||||
|
||||
# Guard: No workspaces registered
|
||||
if ($workspaces | length) == 0 {
|
||||
return (ok [])
|
||||
}
|
||||
|
||||
# Pure transformation
|
||||
let result = ($workspaces | each {|ws|
|
||||
{
|
||||
name: $ws.name
|
||||
path: $ws.path
|
||||
active: ($ws.name == $active)
|
||||
last_used: ($ws | get --optional last_used | default "Never")
|
||||
}
|
||||
})
|
||||
|
||||
ok $result
|
||||
}
|
||||
|
||||
# Get active workspace name
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
# Result: {ok: string, err: null} on success; {ok: null, err: message} on error
|
||||
export def workspace-active [] {
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Guard: Config doesn't exist
|
||||
if not ($user_config | path exists) {
|
||||
return (ok "")
|
||||
}
|
||||
|
||||
# Guard: File exists, read directly
|
||||
let active_name = (open $user_config | get --optional active_workspace | default "")
|
||||
ok $active_name
|
||||
}
|
||||
|
||||
# Get workspace info by name
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
|
||||
export def workspace-info [name: string] {
|
||||
# Guard: Input validation
|
||||
if ($name | is-empty) {
|
||||
return (err "workspace name is required")
|
||||
}
|
||||
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Guard: Config doesn't exist
|
||||
if not ($user_config | path exists) {
|
||||
return (ok {name: $name, path: "", exists: false})
|
||||
}
|
||||
|
||||
# Guard: File exists, read directly
|
||||
let config = (open $user_config)
|
||||
let workspaces = ($config | get --optional workspaces | default [])
|
||||
let ws = ($workspaces | where { $in.name == $name } | first)
|
||||
|
||||
# Guard: Workspace not found
|
||||
if ($ws | is-empty) {
|
||||
return (ok {name: $name, path: "", exists: false, default_infra: "", infrastructures: []})
|
||||
}
|
||||
|
||||
# Collect infra dirs with server counts
|
||||
let infra_root = ($ws.path | path join 'infra')
|
||||
let infrastructures = if ($infra_root | path exists) {
|
||||
ls $infra_root
|
||||
| where type == 'dir'
|
||||
| each {|inf|
|
||||
let inf_name = ($inf.name | path basename)
|
||||
let sf_direct = ($infra_root | path join $inf_name | path join 'servers.ncl')
|
||||
let sf_defs = ($infra_root | path join $inf_name | path join 'defs' | path join 'servers.ncl')
|
||||
let sf = if ($sf_direct | path exists) { $sf_direct } else { $sf_defs }
|
||||
let server_count = if ($sf | path exists) {
|
||||
open $sf --raw | split row "\n" | where {|l| $l =~ 'hostname\s*=\s*"' } | length
|
||||
} else { 0 }
|
||||
{ name: $inf_name, servers: $server_count }
|
||||
}
|
||||
} else { [] }
|
||||
|
||||
ok {
|
||||
name: $ws.name
|
||||
path: $ws.path
|
||||
exists: true
|
||||
last_used: ($ws | get --optional last_used | default "Never")
|
||||
default_infra: ($ws | get --optional default_infra | default "")
|
||||
infrastructures: $infrastructures
|
||||
}
|
||||
}
|
||||
|
||||
# Quick status check (orchestrator health + active workspace)
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
|
||||
export def status-quick [] {
|
||||
# Guard: HTTP check with do/complete pattern (no try-catch)
|
||||
let health_result = (do { http get --max-time 2sec "http://localhost:9090/health" } | complete)
|
||||
let orch_health = if ($health_result.exit_code == 0) { $health_result.stdout } else { null }
|
||||
let orch_status = if ($orch_health != null) { "running" } else { "stopped" }
|
||||
|
||||
# Guard: Get active workspace safely
|
||||
let ws_result = (workspace-active)
|
||||
let active_ws = (if (is-ok $ws_result) { $ws_result.ok } else { "" })
|
||||
|
||||
# Pure transformation
|
||||
ok {
|
||||
orchestrator: $orch_status
|
||||
workspace: $active_ws
|
||||
timestamp: (date now | format date "%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
}
|
||||
|
||||
# Display essential environment variables
|
||||
# Rule 1: Explicit types, Rule 8: Pure function (read-only)
|
||||
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
|
||||
export def env-quick [] {
|
||||
# Pure transformation with optional operator
|
||||
let vars = {
|
||||
PROVISIONING_ROOT: ($env.PROVISIONING_ROOT? | default "not set")
|
||||
PROVISIONING_ENV: ($env.PROVISIONING_ENV? | default "not set")
|
||||
PROVISIONING_DEBUG: ($env.PROVISIONING_DEBUG? | default "false")
|
||||
HOME: $env.HOME
|
||||
PWD: $env.PWD
|
||||
}
|
||||
|
||||
ok $vars
|
||||
}
|
||||
|
||||
# Show quick help for fast-path commands
|
||||
# Rule 1: Explicit types, Rule 8: Pure function
|
||||
export def quick-help [] {
|
||||
"Provisioning CLI - Fast Path Commands
|
||||
|
||||
Quick Commands (< 100ms):
|
||||
workspace list List all registered workspaces
|
||||
workspace active Show currently active workspace
|
||||
status Quick health check
|
||||
env Show essential environment variables
|
||||
help [command] Show help for a command
|
||||
|
||||
For full help:
|
||||
provisioning help Show all available commands
|
||||
provisioning help <command> Show help for specific command"
|
||||
}
|
||||
9
nulib/cli/fastpath/mod.nu
Normal file
9
nulib/cli/fastpath/mod.nu
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# cli/fastpath/ subsystem façade (ADR-026).
|
||||
export use cli/fastpath/lib_minimal.nu [
|
||||
workspace-list
|
||||
workspace-active
|
||||
workspace-info
|
||||
status-quick
|
||||
env-quick
|
||||
quick-help
|
||||
]
|
||||
26
nulib/cli/fastpath/prov-bootstrap.nu
Normal file
26
nulib/cli/fastpath/prov-bootstrap.nu
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env nu
|
||||
# Standalone bootstrap runner — bypasses the dispatcher.
|
||||
# Loads only the modules needed for L1 Hetzner resource provisioning.
|
||||
#
|
||||
# Usage (from provisioning/ dir):
|
||||
# nu core/nulib/scripts/prov-bootstrap.nu -w librecloud_renew --dry-run
|
||||
# nu core/nulib/scripts/prov-bootstrap.nu -w librecloud_renew
|
||||
|
||||
use orchestration/bootstrap.nu *
|
||||
use platform/user/config.nu [get-workspace-path, get-active-workspace-details]
|
||||
use domain/workspace/mod.nu *
|
||||
|
||||
def main [
|
||||
--workspace (-w): string # Workspace name (default: active workspace)
|
||||
--dry-run (-n) # Print what would be created without calling the API
|
||||
] {
|
||||
if ($workspace | is-not-empty) and $dry_run {
|
||||
main bootstrap --workspace $workspace --dry-run
|
||||
} else if ($workspace | is-not-empty) {
|
||||
main bootstrap --workspace $workspace
|
||||
} else if $dry_run {
|
||||
main bootstrap --dry-run
|
||||
} else {
|
||||
main bootstrap
|
||||
}
|
||||
}
|
||||
25
nulib/cli/fastpath/prov-cluster-deploy.nu
Normal file
25
nulib/cli/fastpath/prov-cluster-deploy.nu
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env nu
|
||||
# Standalone cluster-deploy runner — bypasses the dispatcher.
|
||||
# Loads only the modules needed for L3/L4 cluster extension deployment.
|
||||
#
|
||||
# Usage (from provisioning/ dir):
|
||||
# nu core/nulib/scripts/prov-cluster-deploy.nu platform sgoyol -w librecloud_renew --dry-run
|
||||
# nu core/nulib/scripts/prov-cluster-deploy.nu apps sgoyol -w librecloud_renew
|
||||
|
||||
use orchestration/cluster_deploy.nu *
|
||||
use platform/user/config.nu [get-workspace-path, get-active-workspace-details]
|
||||
use domain/workspace/mod.nu *
|
||||
|
||||
def main [
|
||||
layer: string # Deployment layer: platform | apps
|
||||
cluster: string # Cluster name (e.g. sgoyol)
|
||||
--workspace (-w): string # Workspace name (default: active workspace)
|
||||
--dry-run (-n) # Print plan without executing install scripts
|
||||
--kubeconfig (-k): string # Override KUBECONFIG path
|
||||
--secrets-file (-s): string # SOPS-encrypted dotenv file with install secrets
|
||||
] {
|
||||
let ws = ($workspace | default "")
|
||||
let kc = ($kubeconfig | default "")
|
||||
let sf = ($secrets_file | default "")
|
||||
main cluster deploy $layer $cluster --workspace $ws --dry-run=$dry_run --kubeconfig $kc --secrets-file $sf
|
||||
}
|
||||
53
nulib/cli/fastpath/validate-command.nu
Executable file
53
nulib/cli/fastpath/validate-command.nu
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/bin/env nu
|
||||
# Validate if a command exists in commands-registry.ncl
|
||||
# Returns: FOUND|true/false or NOT_FOUND
|
||||
#
|
||||
# Cache: exports registry to ~/.cache/provisioning/commands-registry.json
|
||||
# and reuses it until commands-registry.ncl changes (mtime check).
|
||||
# Typical cold start: ~2s (nickel export). Warm: <50ms (JSON read).
|
||||
|
||||
def main [
|
||||
command_name: string
|
||||
]: nothing -> nothing {
|
||||
let registry_file = ($env.PROVISIONING | path join "core/nulib/commands-registry.ncl")
|
||||
let cache_dir = ($env.HOME | path join ".cache" | path join "provisioning")
|
||||
let cache_file = ($cache_dir | path join "commands-registry.json")
|
||||
|
||||
# Determine if cache is valid (exists and newer than source)
|
||||
let registry_mtime = (ls $registry_file | get 0.modified)
|
||||
let use_cache = if ($cache_file | path exists) {
|
||||
let cache_mtime = (ls $cache_file | get 0.modified)
|
||||
$cache_mtime > $registry_mtime
|
||||
} else { false }
|
||||
|
||||
# Load or rebuild
|
||||
let registry_json = if $use_cache {
|
||||
open --raw $cache_file
|
||||
} else {
|
||||
use tools/nickel/process.nu [ncl-eval-soft, default-ncl-paths]
|
||||
let import_paths = (default-ncl-paths)
|
||||
let result = (ncl-eval-soft $registry_file $import_paths)
|
||||
if ($result | is-empty) {
|
||||
print "ERROR: Failed to export commands-registry.ncl" >&2
|
||||
exit 1
|
||||
}
|
||||
let json_str = ($result | to json)
|
||||
^mkdir -p $cache_dir
|
||||
$json_str | save --force $cache_file
|
||||
$json_str
|
||||
}
|
||||
|
||||
let commands = ($registry_json | from json | get -o commands | default [])
|
||||
|
||||
let matches = ($commands | where {|cmd|
|
||||
let all = ([$cmd.command] | append ($cmd | get -o aliases | default []))
|
||||
$command_name in $all
|
||||
})
|
||||
|
||||
if ($matches | is-empty) {
|
||||
print "NOT_FOUND"
|
||||
} else {
|
||||
let m = ($matches | first)
|
||||
print $"FOUND|($m | get -o requires_daemon | default false)"
|
||||
}
|
||||
}
|
||||
237
nulib/cli/flags.nu
Normal file
237
nulib/cli/flags.nu
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# Common flag parsing and validation
|
||||
# Centralized flag handling to reduce code duplication
|
||||
|
||||
use platform/config/accessor.nu *
|
||||
use platform/workspace/notation.nu *
|
||||
|
||||
# Parse common flags into a normalized record
|
||||
# This eliminates repetitive flag checking across command handlers
|
||||
export def parse_common_flags [flags: record] {
|
||||
{
|
||||
# Version and info flags
|
||||
show_version: (($flags.version? | default false) or ($flags.v? | default false))
|
||||
show_info: ($flags.info? | default false)
|
||||
show_about: ($flags.about? | default false)
|
||||
|
||||
# Debug and metadata flags
|
||||
debug_mode: ($flags.debug? | default false)
|
||||
metadata_mode: ($flags.metadata? | default false)
|
||||
debug_check: ($flags.xc? | default false)
|
||||
debug_remote: ($flags.xr? | default false)
|
||||
debug_log_level: ($flags.xld? | default false)
|
||||
|
||||
# Operation mode flags
|
||||
check_mode: ($flags.check? | default false)
|
||||
upload_inspection: ($flags.upload? | default false)
|
||||
auto_confirm: ($flags.yes? | default false)
|
||||
wait_completion: ($flags.wait? | default false)
|
||||
keep_storage: ($flags.keepstorage? | default false)
|
||||
no_clean: ($flags.nc? | default false)
|
||||
include_notuse: ($flags.include_notuse? | default false)
|
||||
|
||||
# Output and format flags
|
||||
output_format: ($flags.out? | default "")
|
||||
no_titles: ($flags.notitles? | default false)
|
||||
view_mode: ($flags.view? | default false)
|
||||
|
||||
# Path and target flags
|
||||
# Only propagate --infra when explicitly passed; PWD-based detection runs in get_infra
|
||||
infra: ($flags.infra? | default "")
|
||||
infras: ($flags.infras? | default "")
|
||||
settings: ($flags.settings? | default "")
|
||||
outfile: ($flags.outfile? | default "")
|
||||
template: ($flags.template? | default "")
|
||||
select: ($flags.select? | default "")
|
||||
onsel: ($flags.onsel? | default "")
|
||||
serverpos: ($flags.serverpos? | default null)
|
||||
|
||||
# New infrastructure flag
|
||||
new_infra: ($flags.new? | default "")
|
||||
|
||||
# Environment flag
|
||||
environment: ($flags.environment? | default "")
|
||||
|
||||
# Cache control flag
|
||||
no_cache: ($flags.no_cache? | default false)
|
||||
|
||||
# Workspace dependency flags
|
||||
dep_option: ($flags.dep_option? | default "")
|
||||
dep_url: ($flags.dep_url? | default "")
|
||||
|
||||
# Pack command flags
|
||||
dry_run: ($flags.dry_run? | default false)
|
||||
force: ($flags.force? | default false)
|
||||
all: ($flags.all? | default false)
|
||||
keep_latest: ($flags.keep_latest? | default null)
|
||||
|
||||
# Workspace command flags
|
||||
activate: ($flags.activate? | default false)
|
||||
interactive: ($flags.interactive? | default false)
|
||||
workspace: ($flags.ws? | default ($flags.workspace? | default ""))
|
||||
|
||||
# Infrastructure-from-Code flags
|
||||
pretty_print: ($flags.pretty? | default false)
|
||||
org: ($flags.org? | default "")
|
||||
apply_changes: ($flags.apply? | default false)
|
||||
verbose_output: ($flags.verbose? | default false)
|
||||
|
||||
# Platform service flags
|
||||
services: ($flags.services? | default "")
|
||||
}
|
||||
}
|
||||
|
||||
# Build standardized module arguments string
|
||||
# Converts parsed flags into command-line argument string
|
||||
export def build_module_args [
|
||||
flags: record
|
||||
extra: string = ""
|
||||
] {
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let use_upload = if ($flags.upload_inspection? | default false) { "--upload " } else { "" }
|
||||
let use_yes = if $flags.auto_confirm { "--yes " } else { "" }
|
||||
let use_wait = if $flags.wait_completion { "--wait " } else { "" }
|
||||
let use_keepstorage = if $flags.keep_storage { "--keepstorage " } else { "" }
|
||||
let use_include_notuse = if $flags.include_notuse { "--include_notuse " } else { "" }
|
||||
|
||||
let str_infra = if ($flags.infra | is-not-empty) {
|
||||
$"--infra ($flags.infra) "
|
||||
} else { "" }
|
||||
|
||||
let str_out = if ($flags.output_format | is-not-empty) {
|
||||
$"--out ($flags.output_format) "
|
||||
} else { "" }
|
||||
|
||||
let str_outfile = if ($flags.outfile | is-not-empty) {
|
||||
$"--outfile ($flags.outfile) "
|
||||
} else { "" }
|
||||
|
||||
let str_template = if ($flags.template | is-not-empty) {
|
||||
$"--template ($flags.template) "
|
||||
} else { "" }
|
||||
|
||||
let str_select = if ($flags.select | is-not-empty) {
|
||||
$"--select ($flags.select) "
|
||||
} else { "" }
|
||||
|
||||
let str_dep_option = if ($flags.dep_option? | default "" | is-not-empty) {
|
||||
$"--dep-option ($flags.dep_option) "
|
||||
} else { "" }
|
||||
|
||||
let str_dep_url = if ($flags.dep_url? | default "" | is-not-empty) {
|
||||
$"--dep-url ($flags.dep_url) "
|
||||
} else { "" }
|
||||
|
||||
let use_dry_run = if ($flags.dry_run? | default false) { "--dry-run " } else { "" }
|
||||
let use_force = if ($flags.force? | default false) { "--force " } else { "" }
|
||||
let use_all = if ($flags.all? | default false) { "--all " } else { "" }
|
||||
let str_keep_latest = if ($flags.keep_latest? | default null | is-not-empty) {
|
||||
$"--keep-latest ($flags.keep_latest) "
|
||||
} else { "" }
|
||||
|
||||
let use_no_titles = if ($flags.no_titles? | default false) { "--notitles " } else { "" }
|
||||
let use_no_cache = if ($flags.no_cache? | default false) { "--no-cache " } else { "" }
|
||||
|
||||
# Combine all flags with extra arguments
|
||||
# Ensure extra has trailing space if not empty
|
||||
let extra_with_space = if ($extra | is-not-empty) { $"($extra) " } else { "" }
|
||||
|
||||
[
|
||||
$extra_with_space
|
||||
$str_infra
|
||||
$use_check
|
||||
$use_upload
|
||||
$use_yes
|
||||
$use_wait
|
||||
$use_keepstorage
|
||||
$str_out
|
||||
$str_outfile
|
||||
$str_template
|
||||
$str_select
|
||||
$str_dep_option
|
||||
$str_dep_url
|
||||
$use_dry_run
|
||||
$use_force
|
||||
$use_all
|
||||
$str_keep_latest
|
||||
$use_include_notuse
|
||||
$use_no_titles
|
||||
$use_no_cache
|
||||
] | str join "" | str trim
|
||||
}
|
||||
|
||||
# Set environment variables based on parsed flags
|
||||
export def set_debug_env [flags: record] {
|
||||
let debug_mode = ($flags | get --optional debug_mode)
|
||||
if ($debug_mode | is-not-empty) and $debug_mode {
|
||||
$env.PROVISIONING_DEBUG = true
|
||||
}
|
||||
|
||||
let metadata_mode = ($flags | get --optional metadata_mode)
|
||||
if ($metadata_mode | is-not-empty) and $metadata_mode {
|
||||
$env.PROVISIONING_METADATA = true
|
||||
}
|
||||
|
||||
let debug_check = ($flags | get --optional debug_check)
|
||||
if ($debug_check | is-not-empty) and $debug_check {
|
||||
$env.PROVISIONING_DEBUG_CHECK = true
|
||||
}
|
||||
|
||||
let debug_remote = ($flags | get --optional debug_remote)
|
||||
if ($debug_remote | is-not-empty) and $debug_remote {
|
||||
$env.PROVISIONING_DEBUG_REMOTE = true
|
||||
}
|
||||
|
||||
let debug_log_level = ($flags | get --optional debug_log_level)
|
||||
if ($debug_log_level | is-not-empty) {
|
||||
$env.PROVISIONING_LOG_LEVEL = "debug"
|
||||
}
|
||||
|
||||
let output_format = ($flags | get --optional output_format)
|
||||
if ($output_format | is-not-empty) {
|
||||
$env.PROVISIONING_OUT = $output_format
|
||||
$env.PROVISIONING_NO_TERMINAL = true
|
||||
}
|
||||
|
||||
let environment = ($flags | get --optional environment)
|
||||
if ($environment | is-not-empty) {
|
||||
$env.PROVISIONING_ENV = $environment
|
||||
}
|
||||
|
||||
# Set PROVISIONING_INFRA env var from infra flag if provided
|
||||
# This supports both direct env var and --infra flag methods
|
||||
let infra = ($flags | get --optional infra)
|
||||
if ($infra | is-not-empty) {
|
||||
$env.PROVISIONING_INFRA = $infra
|
||||
}
|
||||
}
|
||||
|
||||
# Get debug flag for module execution
|
||||
export def get_debug_flag [flags: record] {
|
||||
if $flags.debug_mode or ($env.PROVISIONING_DEBUG? | default false) {
|
||||
"-x"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
# Extract workspace and infrastructure from workspace flag
|
||||
# Handles parsing workspace:infra notation
|
||||
export def extract-workspace-infra-from-flags [flags: record] {
|
||||
let ws_flag = ($flags | get --optional workspace)
|
||||
|
||||
if ($ws_flag | is-empty) {
|
||||
return { workspace: null, infra: ($flags | get --optional infra) }
|
||||
}
|
||||
|
||||
# Parse workspace:infra notation
|
||||
let parsed = (parse-workspace-infra-notation $ws_flag)
|
||||
|
||||
{
|
||||
workspace: $parsed.workspace
|
||||
infra: (if ($parsed.infra | is-not-empty) {
|
||||
$parsed.infra
|
||||
} else {
|
||||
($flags | get --optional infra)
|
||||
})
|
||||
}
|
||||
}
|
||||
91
nulib/cli/generate.nu
Normal file
91
nulib/cli/generate.nu
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# REMOVED: use lib_provisioning * - causes circular import (already loaded by main provisioning script)
|
||||
use orchestration/taskservs/utils.nu *
|
||||
use orchestration/taskservs/handlers.nu *
|
||||
use domain/ssh/lib.nu *
|
||||
use platform/config/accessor.nu *
|
||||
|
||||
# > TaskServs generate
|
||||
export def "main generate" [
|
||||
task_name?: string # task in settings
|
||||
server?: string # Server hostname in settings
|
||||
...args # Args for generate command
|
||||
--infra (-i): string # Infra directory
|
||||
--settings (-s): string # Settings path
|
||||
--iptype: string = "public" # Ip type to connect
|
||||
--outfile (-o): string # Output file
|
||||
--taskserv_pos (-p): int # Server position in settings
|
||||
--check (-c) # Only check mode no taskservs will be generated
|
||||
--wait (-w) # Wait taskservs to be generated
|
||||
--select: string # Select with task as option
|
||||
--debug (-x) # Use Debug mode
|
||||
--xm # Debug with PROVISIONING_METADATA
|
||||
--xc # Debuc for task and services locally PROVISIONING_DEBUG_CHECK
|
||||
--xr # Debug for remote taskservs PROVISIONING_DEBUG_REMOTE
|
||||
--xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug
|
||||
--metadata # Error with metadata (-xm)
|
||||
--notitles # not tittles
|
||||
--helpinfo (-h) # For more details use options "help" (no dashes)
|
||||
--out: string # Print Output format: json, yaml, text (default)
|
||||
] {
|
||||
if ($out | is-not-empty) {
|
||||
set-provisioning-out $out
|
||||
set-provisioning-no-terminal true
|
||||
}
|
||||
provisioning_init $helpinfo "taskserv generate" ([($task_name | default "") ($server | default "")] | append $args)
|
||||
if $debug { set-debug-enabled true }
|
||||
if $metadata { set-metadata-enabled true }
|
||||
let curr_settings = (find_get_settings --infra $infra --settings $settings)
|
||||
let args_result = (do { (get-provisioning-args) | split row " " | get 0 } | complete)
|
||||
let task = if $args_result.exit_code == 0 { $args_result.stdout } else { null }
|
||||
let options = if ($args | length) > 0 {
|
||||
$args
|
||||
} else {
|
||||
let str_task = ((get-provisioning-args) | str replace $"($task) " "" |
|
||||
str replace $"($task_name) " "" | str replace $"($server) " "")
|
||||
let st_result = (do { $str_task | split row "-" | get 0 } | complete)
|
||||
let str_task_result = if $st_result.exit_code == 0 { $st_result.stdout } else { "" }
|
||||
($str_task_result | str trim)
|
||||
}
|
||||
let other = if ($args | length) > 0 { ($args| skip 1) } else { "" }
|
||||
let ops = $"((get-provisioning-args)) " | str replace $"($task_name) " "" | str trim
|
||||
#print "GENEREATE"
|
||||
# "/wuwei/repo-cnz/src/provisioning/taskservs/oci-reg/generate/defs.toml"
|
||||
#exit
|
||||
let run_generate = {
|
||||
let curr_settings = (settings_with_env $curr_settings)
|
||||
set-wk-cnprov $curr_settings.wk_path
|
||||
let arr_task = if $task_name == null or $task_name == "" or $task_name == "-" { [] } else { $task_name | split row "/" }
|
||||
let match_task = if ($arr_task | length) == 0 {
|
||||
""
|
||||
} else {
|
||||
let mt_result = (do { $arr_task | get 0 } | complete)
|
||||
if $mt_result.exit_code == 0 { $mt_result.stdout } else { null }
|
||||
}
|
||||
let match_task_profile = if ($arr_task | length) < 2 {
|
||||
""
|
||||
} else {
|
||||
let mtp_result = (do { $arr_task | get 1 } | complete)
|
||||
if $mtp_result.exit_code == 0 { $mtp_result.stdout } else { null }
|
||||
}
|
||||
let match_server = if $server == null or $server == "" { "" } else { $server}
|
||||
on_taskservs $curr_settings $match_task $match_task_profile $match_server $iptype $check
|
||||
}
|
||||
match $task {
|
||||
"" if $task_name == "h" => {
|
||||
^$"((get-provisioning-name))" -mod taskserv update help --notitles
|
||||
},
|
||||
"" if $task_name == "help" => {
|
||||
^$"((get-provisioning-name))" -mod taskserv update --help
|
||||
_print (provisioning_options "update")
|
||||
},
|
||||
"g" | "generate" | "" => {
|
||||
let result = desktop_run_notify $"((get-provisioning-name)) taskservs generate" "-> " $run_generate --timeout 11sec
|
||||
},
|
||||
_ => {
|
||||
if $task_name != "" {_print $"🛑 invalid_option ($task_name)" }
|
||||
_print $"\nUse (_ansi blue_bold)((get-provisioning-name)) -h(_ansi reset) for help on commands and options"
|
||||
}
|
||||
}
|
||||
# "" | "generate"
|
||||
#if not $env.PROVISIONING_DEBUG { end_run "" }
|
||||
}
|
||||
48
nulib/cli/guide.nu
Normal file
48
nulib/cli/guide.nu
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env nu
|
||||
# Thin entry for guide | shortcuts | quickstart commands.
|
||||
# Loads only commands/guides.nu.
|
||||
|
||||
export-env {
|
||||
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 }
|
||||
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
|
||||
$env.NU_LIB_DIRS = ([
|
||||
"/opt/provisioning/core/nulib"
|
||||
"/usr/local/provisioning/core/nulib"
|
||||
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
|
||||
let _coerce = {|raw| $raw == "true" or $raw == "1" }
|
||||
let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "")
|
||||
if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) {
|
||||
$env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles)
|
||||
}
|
||||
let raw_debug = ($env.PROVISIONING_DEBUG? | default "")
|
||||
if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) {
|
||||
$env.PROVISIONING_DEBUG = (do $_coerce $raw_debug)
|
||||
}
|
||||
}
|
||||
|
||||
use cli/flags.nu [parse_common_flags]
|
||||
use cli/handlers/guides.nu *
|
||||
|
||||
def main [
|
||||
...args: string
|
||||
--infra (-i): string = ""
|
||||
--out: string = ""
|
||||
--debug (-x)
|
||||
--yes (-y)
|
||||
--check (-c)
|
||||
--notitles
|
||||
--verbose
|
||||
|
||||
]: nothing -> nothing {
|
||||
if $debug { $env.PROVISIONING_DEBUG = true }
|
||||
let cmd = ($args | get 0? | default "")
|
||||
let ops = ($args | skip 1 | str join " ")
|
||||
let flags = (parse_common_flags {
|
||||
debug: $debug, out: ($out | default ""), notitles: $notitles,
|
||||
infra: ($infra | default ""), yes: $yes, check: $check, verbose: $verbose
|
||||
})
|
||||
handle_guide_command $cmd $ops $flags
|
||||
}
|
||||
461
nulib/cli/handlers/authentication.nu
Normal file
461
nulib/cli/handlers/authentication.nu
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
# Authentication Command Handlers
|
||||
# Handles: auth login, auth logout, auth status, auth mfa, etc.
|
||||
|
||||
use cli/flags.nu *
|
||||
use platform/plugins/auth.nu *
|
||||
|
||||
# Main authentication command dispatcher
|
||||
export def handle_authentication_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
match $command {
|
||||
"auth" | "login" | "whoami" | "mfa" => {
|
||||
let subcommand = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else if $command == "login" {
|
||||
"login"
|
||||
} else if $command == "whoami" {
|
||||
"status"
|
||||
} else if $command == "mfa" {
|
||||
"mfa"
|
||||
} else {
|
||||
"status"
|
||||
}
|
||||
|
||||
let remaining_ops = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | skip 1 | str join " ")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
match $subcommand {
|
||||
"login" => { handle_auth_login $remaining_ops $flags }
|
||||
"logout" => { handle_auth_logout $flags }
|
||||
"status" | "whoami" => { handle_auth_status $flags }
|
||||
"sessions" | "list" => { handle_auth_sessions $flags }
|
||||
"refresh" => { handle_auth_refresh $flags }
|
||||
"mfa" | "mfa-enroll" | "mfa-verify" => {
|
||||
handle_auth_mfa $subcommand $remaining_ops $flags
|
||||
}
|
||||
"help" => { show_auth_help }
|
||||
_ => {
|
||||
print $"❌ Unknown auth subcommand: ($subcommand)"
|
||||
print ""
|
||||
print "Available auth subcommands:"
|
||||
print " login - Login with credentials"
|
||||
print " logout - Logout (revoke session)"
|
||||
print " status - Show current authentication status"
|
||||
print " whoami - Alias for status"
|
||||
print " sessions - List active sessions"
|
||||
print " list - Alias for sessions"
|
||||
print " refresh - Refresh authentication token"
|
||||
print " mfa - MFA operations"
|
||||
print " mfa-enroll - Enroll in MFA"
|
||||
print " mfa-verify - Verify MFA code"
|
||||
print " help - Show this help message"
|
||||
print ""
|
||||
print "Use 'provisioning auth help' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown authentication command: ($command)"
|
||||
print ""
|
||||
print "Available authentication commands:"
|
||||
print " login - Login with username and password"
|
||||
print " logout - Logout (revoke session)"
|
||||
print " status | whoami - Show current user and session status"
|
||||
print " sessions | list - List active sessions"
|
||||
print " refresh - Refresh authentication token"
|
||||
print " mfa - Multi-factor authentication"
|
||||
print " mfa-enroll - Enroll in MFA (TOTP or WebAuthn)"
|
||||
print " mfa-verify - Verify MFA code"
|
||||
print " help - Show auth help"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning auth login"
|
||||
print " provisioning auth status"
|
||||
print " provisioning mfa totp enroll"
|
||||
print " provisioning mfa verify --code 123456"
|
||||
print ""
|
||||
print "Use 'provisioning help authentication' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Login with username and password
|
||||
def handle_auth_login [ops: string, flags: record] {
|
||||
let username = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
input $"(_ansi cyan)Username:(_ansi reset) "
|
||||
}
|
||||
|
||||
if ($username | is-empty) {
|
||||
print $"(_ansi red)❌ Username is required(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prompt for password securely
|
||||
print $"(_ansi cyan)Password for ($username):(_ansi reset) "
|
||||
let password = (input --suppress-output)
|
||||
|
||||
if ($password | is-empty) {
|
||||
print $"(_ansi red)❌ Password is required(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print $"\n(_ansi cyan)Authenticating...(_ansi reset)"
|
||||
|
||||
let result = (do -i {
|
||||
plugin-login $username $password
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"\n(_ansi green_bold)✓ Login successful!(_ansi reset)\n"
|
||||
print $"(_ansi cyan)User:(_ansi reset) ($result.user? | default $username)"
|
||||
print $"(_ansi cyan)Role:(_ansi reset) ($result.role? | default 'N/A')"
|
||||
print $"(_ansi cyan)Expires:(_ansi reset) ($result.expires_at? | default 'N/A')"
|
||||
print $"(_ansi cyan)MFA:(_ansi reset) ($result.mfa_enabled? | default false)"
|
||||
|
||||
if ($result.mfa_required? | default false) {
|
||||
print $"\n(_ansi yellow)⚠️ MFA verification required(_ansi reset)"
|
||||
print $"(_ansi cyan)Use: provisioning auth mfa verify --code <6-digit-code>(_ansi reset)"
|
||||
} else {
|
||||
print $"\n(_ansi green)Session active and ready(_ansi reset)"
|
||||
}
|
||||
} else {
|
||||
print $"\n(_ansi red_bold)❌ Login failed(_ansi reset)"
|
||||
print $"(_ansi red)Authentication error occurred(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Logout and clear tokens
|
||||
def handle_auth_logout [flags: record] {
|
||||
print $"(_ansi cyan)Logging out...(_ansi reset)"
|
||||
|
||||
let result = (do -i {
|
||||
plugin-logout
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"\n(_ansi green_bold)✓ Logged out successfully(_ansi reset)"
|
||||
print $"(_ansi default_dimmed)All tokens have been cleared(_ansi reset)"
|
||||
} else {
|
||||
print $"\n(_ansi yellow)⚠️ Logout completed with warnings(_ansi reset)"
|
||||
print $"(_ansi default_dimmed)Token cleanup may have failed(_ansi reset)"
|
||||
}
|
||||
}
|
||||
|
||||
# Show authentication status
|
||||
def handle_auth_status [flags: record] {
|
||||
print $"(_ansi cyan_bold)Authentication Status(_ansi reset)\n"
|
||||
|
||||
let status = (do -i {
|
||||
plugin-auth-status
|
||||
})
|
||||
|
||||
if $status != null {
|
||||
if ($status.authenticated? | default false) {
|
||||
print $"(_ansi green)Status:(_ansi reset) Authenticated ✓"
|
||||
print $"(_ansi cyan)User:(_ansi reset) ($status.user? | default 'N/A')"
|
||||
print $"(_ansi cyan)Role:(_ansi reset) ($status.role? | default 'N/A')"
|
||||
print $"(_ansi cyan)Expires at:(_ansi reset) ($status.expires_at? | default 'N/A')"
|
||||
print $"(_ansi cyan)MFA enabled:(_ansi reset) ($status.mfa_enabled? | default false)"
|
||||
|
||||
if ($status.permissions? | is-not-empty) {
|
||||
print $"\n(_ansi cyan_bold)Permissions:(_ansi reset)"
|
||||
for perm in $status.permissions {
|
||||
print $" • ($perm)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($status.warning? | is-not-empty) {
|
||||
print $"\n(_ansi yellow)⚠️ ($status.warning)(_ansi reset)"
|
||||
}
|
||||
} else {
|
||||
print $"(_ansi red)Status:(_ansi reset) Not authenticated ✗"
|
||||
print $"(_ansi cyan)Message:(_ansi reset) ($status.message? | default 'No active session')"
|
||||
print $"\n(_ansi default_dimmed)Use 'provisioning auth login' to authenticate(_ansi reset)"
|
||||
}
|
||||
|
||||
# Show plugin mode
|
||||
print $"\n(_ansi default_dimmed)Mode: ($status.mode? | default 'unknown')(_ansi reset)"
|
||||
} else {
|
||||
print $"(_ansi red)❌ Failed to get auth status(_ansi reset)"
|
||||
print $"(_ansi red)Status check error occurred(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# List active sessions
|
||||
def handle_auth_sessions [flags: record] {
|
||||
print $"(_ansi cyan_bold)Active Sessions(_ansi reset)\n"
|
||||
|
||||
let sessions = (do -i {
|
||||
plugin-sessions
|
||||
})
|
||||
|
||||
if $sessions != null {
|
||||
if ($sessions | length) == 0 {
|
||||
print $"(_ansi yellow)No active sessions found(_ansi reset)"
|
||||
return
|
||||
}
|
||||
|
||||
$sessions | table -e
|
||||
} else {
|
||||
print $"(_ansi red)❌ Failed to list sessions(_ansi reset)"
|
||||
print $"(_ansi red)Session listing error occurred(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Refresh access token
|
||||
def handle_auth_refresh [flags: record] {
|
||||
print $"(_ansi cyan)Refreshing access token...(_ansi reset)"
|
||||
|
||||
let result = (do -i {
|
||||
plugin-verify
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
if ($result.valid? | default false) {
|
||||
print $"(_ansi green)✓ Token is still valid(_ansi reset)"
|
||||
print $"(_ansi cyan)Expires:(_ansi reset) ($result.expires_at? | default 'N/A')"
|
||||
} else {
|
||||
print $"(_ansi yellow)Token expired or invalid, please login again(_ansi reset)"
|
||||
}
|
||||
} else {
|
||||
print $"(_ansi red)❌ Token verification failed(_ansi reset)"
|
||||
print $"(_ansi red)Verification error occurred(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Handle MFA commands
|
||||
def handle_auth_mfa [subcommand: string, ops: string, flags: record] {
|
||||
let mfa_action = if $subcommand == "mfa" {
|
||||
if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
"help"
|
||||
}
|
||||
} else if $subcommand == "mfa-enroll" {
|
||||
"enroll"
|
||||
} else if $subcommand == "mfa-verify" {
|
||||
"verify"
|
||||
} else {
|
||||
"help"
|
||||
}
|
||||
|
||||
let remaining_ops = if $subcommand == "mfa" and ($ops | is-not-empty) {
|
||||
($ops | split row " " | skip 1 | str join " ")
|
||||
} else {
|
||||
$ops
|
||||
}
|
||||
|
||||
match $mfa_action {
|
||||
"enroll" | "enable" | "setup" => { handle_mfa_enroll $remaining_ops $flags }
|
||||
"verify" | "check" => { handle_mfa_verify $remaining_ops $flags }
|
||||
"help" => { show_mfa_help }
|
||||
_ => {
|
||||
print $"❌ Unknown MFA action: ($mfa_action)"
|
||||
print "Use 'provisioning auth mfa help' for available commands"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Enroll in MFA
|
||||
def handle_mfa_enroll [ops: string, flags: record] {
|
||||
let mfa_type = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
"totp"
|
||||
}
|
||||
|
||||
if $mfa_type not-in ["totp" "webauthn"] {
|
||||
print $"(_ansi red)❌ Invalid MFA type: ($mfa_type)(_ansi reset)"
|
||||
print $"(_ansi cyan)Valid types: totp, webauthn(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print $"(_ansi cyan_bold)Enrolling in MFA ($mfa_type)...(_ansi reset)\n"
|
||||
|
||||
let result = (do -i {
|
||||
plugin-mfa-enroll --type $mfa_type
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
if $mfa_type == "totp" {
|
||||
print $"(_ansi green_bold)✓ TOTP enrollment initiated(_ansi reset)\n"
|
||||
print $"(_ansi cyan_bold)Secret:(_ansi reset) ($result.secret? | default 'N/A')\n"
|
||||
|
||||
if ($result.qr_code? | is-not-empty) {
|
||||
print $"(_ansi cyan_bold)QR Code:(_ansi reset)"
|
||||
print $result.qr_code
|
||||
print ""
|
||||
}
|
||||
|
||||
print $"(_ansi yellow_bold)📱 Setup Instructions:(_ansi reset)"
|
||||
print $" 1. Open your authenticator app (Google Authenticator, Authy, etc.)"
|
||||
print $" 2. Scan the QR code above or manually enter the secret"
|
||||
print $" 3. Verify with the 6-digit code:\n"
|
||||
print $" (_ansi cyan)provisioning auth mfa verify --code <6-digit-code>(_ansi reset)\n"
|
||||
} else {
|
||||
print $"(_ansi green_bold)✓ WebAuthn enrollment initiated(_ansi reset)\n"
|
||||
print $"(_ansi yellow_bold)🔑 Setup Instructions:(_ansi reset)"
|
||||
print $" 1. Follow browser prompts to register your security key"
|
||||
print $" 2. Touch your YubiKey, use Touch ID, or Windows Hello"
|
||||
print $" 3. Complete registration in the web interface\n"
|
||||
}
|
||||
} else {
|
||||
print $"\n(_ansi red_bold)❌ MFA enrollment failed(_ansi reset)"
|
||||
print $"(_ansi red)Enrollment error occurred(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Verify MFA code
|
||||
def handle_mfa_verify [ops: string, flags: record] {
|
||||
let code = if ($flags.code? | is-not-empty) {
|
||||
$flags.code
|
||||
} else if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
input $"(_ansi cyan)Enter 6-digit MFA code:(_ansi reset) "
|
||||
}
|
||||
|
||||
if ($code | is-empty) {
|
||||
print $"(_ansi red)❌ MFA code is required(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
print $"(_ansi cyan)Verifying MFA code...(_ansi reset)"
|
||||
|
||||
let result = (do -i {
|
||||
plugin-mfa-verify $code
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"\n(_ansi green_bold)✓ MFA verified successfully!(_ansi reset)\n"
|
||||
print $"(_ansi cyan)Status:(_ansi reset) ($result.status? | default 'Active')"
|
||||
|
||||
if ($result.backup_codes? | is-not-empty) {
|
||||
print $"\n(_ansi yellow_bold)⚠️ Backup Codes \(save these securely\):(_ansi reset)"
|
||||
for code in $result.backup_codes {
|
||||
print $" • ($code)"
|
||||
}
|
||||
print $"\n(_ansi default_dimmed)Use these codes if you lose access to your MFA device(_ansi reset)"
|
||||
}
|
||||
} else {
|
||||
print $"\n(_ansi red_bold)❌ MFA verification failed(_ansi reset)"
|
||||
print $"(_ansi red)Verification error occurred(_ansi reset)"
|
||||
print $"\n(_ansi yellow)💡 Tip: Make sure the code is current (30-second window)(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Show authentication help
|
||||
def show_auth_help [] {
|
||||
print $"
|
||||
(_ansi cyan_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
|
||||
(_ansi cyan_bold)║(_ansi reset) 🔐 AUTHENTICATION COMMANDS (_ansi cyan_bold)║(_ansi reset)
|
||||
(_ansi cyan_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[Session Management](_ansi reset)
|
||||
(_ansi blue)auth login <username>(_ansi reset) Login and store JWT tokens
|
||||
(_ansi blue)auth logout(_ansi reset) Logout and clear tokens
|
||||
(_ansi blue)auth status(_ansi reset) Show current authentication status
|
||||
(_ansi blue)auth sessions(_ansi reset) List active sessions
|
||||
(_ansi blue)auth refresh(_ansi reset) Verify/refresh token
|
||||
|
||||
(_ansi green_bold)[Multi-Factor Authentication](_ansi reset)
|
||||
(_ansi blue)auth mfa enroll <type>(_ansi reset) Enroll in MFA (totp or webauthn)
|
||||
(_ansi blue)auth mfa verify --code <code>(_ansi reset) Verify MFA code
|
||||
|
||||
(_ansi green_bold)EXAMPLES(_ansi reset)
|
||||
|
||||
# Login interactively
|
||||
provisioning auth login
|
||||
|
||||
# Login with username
|
||||
provisioning auth login admin
|
||||
|
||||
# Check authentication status
|
||||
provisioning auth status
|
||||
provisioning whoami (_ansi default_dimmed)# shortcut(_ansi reset)
|
||||
|
||||
# Enroll in TOTP MFA
|
||||
provisioning auth mfa enroll totp
|
||||
|
||||
# Verify MFA code
|
||||
provisioning auth mfa verify --code 123456
|
||||
|
||||
# List active sessions
|
||||
provisioning auth sessions
|
||||
|
||||
# Logout
|
||||
provisioning auth logout
|
||||
|
||||
(_ansi green_bold)SHORTCUTS(_ansi reset)
|
||||
|
||||
login → auth login
|
||||
logout → auth logout
|
||||
whoami → auth status
|
||||
mfa → auth mfa
|
||||
mfa-enroll → auth mfa enroll
|
||||
mfa-verify → auth mfa verify
|
||||
|
||||
(_ansi default_dimmed)💡 Authentication uses JWT tokens with RS256 signing
|
||||
Tokens are stored securely and expire after 15 minutes
|
||||
MFA is required for sensitive operations in production(_ansi reset)
|
||||
"
|
||||
}
|
||||
|
||||
# Show MFA help
|
||||
def show_mfa_help [] {
|
||||
print $"
|
||||
(_ansi cyan_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
|
||||
(_ansi cyan_bold)║(_ansi reset) 🔐 MULTI-FACTOR AUTHENTICATION (_ansi cyan_bold)║(_ansi reset)
|
||||
(_ansi cyan_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[MFA Types](_ansi reset)
|
||||
|
||||
(_ansi blue)TOTP (Time-based One-Time Password)(_ansi reset)
|
||||
• 6-digit codes that change every 30 seconds
|
||||
• Works with Google Authenticator, Authy, etc.
|
||||
• No internet required after setup
|
||||
|
||||
(_ansi blue)WebAuthn/FIDO2(_ansi reset)
|
||||
• Hardware security keys (YubiKey)
|
||||
• Biometric authentication (Touch ID, Windows Hello)
|
||||
• Phishing-resistant
|
||||
|
||||
(_ansi green_bold)[Commands](_ansi reset)
|
||||
|
||||
(_ansi blue)auth mfa enroll totp(_ansi reset) Enroll in TOTP MFA
|
||||
(_ansi blue)auth mfa enroll webauthn(_ansi reset) Enroll in WebAuthn MFA
|
||||
(_ansi blue)auth mfa verify --code <code>(_ansi reset) Verify MFA code
|
||||
|
||||
(_ansi green_bold)EXAMPLES(_ansi reset)
|
||||
|
||||
# Enroll in TOTP
|
||||
provisioning auth mfa enroll totp
|
||||
|
||||
# Scan QR code with authenticator app
|
||||
# Then verify with 6-digit code
|
||||
provisioning auth mfa verify --code 123456
|
||||
|
||||
# Enroll in WebAuthn
|
||||
provisioning auth mfa enroll webauthn
|
||||
|
||||
(_ansi default_dimmed)💡 MFA enrollment requires active authentication session
|
||||
Use 'provisioning auth login' first if not authenticated(_ansi reset)
|
||||
"
|
||||
}
|
||||
79
nulib/cli/handlers/build.nu
Normal file
79
nulib/cli/handlers/build.nu
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Build command handler — directly invoke image subcommand handlers
|
||||
|
||||
export def handle_build_command [command: string, ops: string, flags: record] {
|
||||
use orchestration/images/create.nu *
|
||||
use orchestration/images/list.nu *
|
||||
use orchestration/images/update.nu *
|
||||
use orchestration/images/delete.nu *
|
||||
use orchestration/images/state.nu *
|
||||
use orchestration/images/watch.nu *
|
||||
|
||||
# Normalize: strip leading "image " prefix when invoked via "build build" registry path
|
||||
let image_ops = if $command == "build" {
|
||||
if ($ops | str starts-with "image ") {
|
||||
$ops | str replace "image " ""
|
||||
} else {
|
||||
if ($ops | str trim) == "image" {
|
||||
"help"
|
||||
} else {
|
||||
if ($ops | is-empty) { "help" } else { $ops }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# command == "image" from "bi" / "build-image" shortcut
|
||||
if ($ops | is-empty) { "help" } else { $ops }
|
||||
}
|
||||
|
||||
# Parse the image_ops to extract subcommand and role
|
||||
let parts = ($image_ops | split row " ")
|
||||
let subcommand = if ($parts | length) > 0 { $parts | get 0 } else { "help" }
|
||||
let role = if ($parts | length) > 1 { $parts | get 1 } else { "" }
|
||||
|
||||
# Extract flag values
|
||||
let check_f = ($flags | get check_mode? | default false)
|
||||
let yes_f = ($flags | get auto_confirm? | default false)
|
||||
let infra_f = ($flags.infra? | default "")
|
||||
let provider_f = ($flags.provider? | default "")
|
||||
|
||||
# Call the appropriate image subcommand handler
|
||||
match $subcommand {
|
||||
"create" | "c" => {
|
||||
image-create $role --infra=$infra_f --check=$check_f
|
||||
}
|
||||
"list" | "l" => {
|
||||
image-list --provider=$provider_f
|
||||
}
|
||||
"update" | "u" => {
|
||||
image-update $role --infra=$infra_f --check=$check_f
|
||||
}
|
||||
"delete" | "d" => {
|
||||
image-delete $role --yes=$yes_f
|
||||
}
|
||||
"state" | "s" => {
|
||||
image-state-list --provider=$infra_f
|
||||
}
|
||||
"watch" | "w" => {
|
||||
image-watch --interval=(($role | into int) | default 30)
|
||||
}
|
||||
"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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
37
nulib/cli/handlers/configuration.nu
Normal file
37
nulib/cli/handlers/configuration.nu
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Configuration Command Handler
|
||||
# Provides configuration management commands
|
||||
|
||||
use platform/config/accessor.nu *
|
||||
|
||||
export def handle_configuration_command [command: string, ops: string, flags: record] {
|
||||
let subcmd = if ($ops | is-empty) { "" } else { $ops | split row " " | first }
|
||||
|
||||
match $subcmd {
|
||||
"config" => {
|
||||
# Initialize user configuration
|
||||
print "🚀 Initializing user configuration"
|
||||
print "=================================="
|
||||
print ""
|
||||
print "Config initialization available"
|
||||
}
|
||||
|
||||
"show" => {
|
||||
print "📋 Current Configuration"
|
||||
print "======================="
|
||||
let cfg = (get-config)
|
||||
print ($cfg | to json)
|
||||
}
|
||||
|
||||
"validate" => {
|
||||
print "✓ Configuration is valid"
|
||||
}
|
||||
|
||||
"reset" => {
|
||||
print "🔄 Configuration reset"
|
||||
}
|
||||
|
||||
_ => {
|
||||
print "Unknown configuration command"
|
||||
}
|
||||
}
|
||||
}
|
||||
73
nulib/cli/handlers/development.nu
Normal file
73
nulib/cli/handlers/development.nu
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Development Command Handlers
|
||||
# Handles: module, layer, version, pack commands
|
||||
|
||||
use cli/flags.nu *
|
||||
# REMOVED: use ../../lib_provisioning * - causes circular import
|
||||
|
||||
# Helper to run module commands
|
||||
def run_module [
|
||||
args: string
|
||||
module: string
|
||||
option?: string
|
||||
--exec
|
||||
] {
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
|
||||
if $exec {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args --notitles
|
||||
} else {
|
||||
^$"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args --notitles
|
||||
}
|
||||
}
|
||||
|
||||
# Main development command dispatcher
|
||||
export def handle_development_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
set_debug_env $flags
|
||||
|
||||
match $command {
|
||||
"module" => { handle_module $ops $flags }
|
||||
"layer" => { handle_layer $ops $flags }
|
||||
"version" => { handle_version $ops $flags }
|
||||
"pack" => { handle_pack $ops $flags }
|
||||
_ => {
|
||||
print $"❌ Unknown development command: ($command)"
|
||||
print ""
|
||||
print "Available development commands:"
|
||||
print " module - Module discovery and management"
|
||||
print " layer - Layer system (explain, show, test, stats)"
|
||||
print " version - Version management (check, show, updates)"
|
||||
print " pack - Packaging (core, provider, list, clean)"
|
||||
print ""
|
||||
print "Use 'provisioning help development' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Module command handler
|
||||
def handle_module [ops: string, flags: record] {
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "module" --exec
|
||||
}
|
||||
|
||||
# Layer command handler
|
||||
def handle_layer [ops: string, flags: record] {
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "layer" --exec
|
||||
}
|
||||
|
||||
# Version command handler
|
||||
def handle_version [ops: string, flags: record] {
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "version" --exec
|
||||
}
|
||||
|
||||
# Pack command handler
|
||||
def handle_pack [ops: string, flags: record] {
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "pack" --exec
|
||||
}
|
||||
79
nulib/cli/handlers/diagnostics.nu
Normal file
79
nulib/cli/handlers/diagnostics.nu
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Diagnostics Command Handlers
|
||||
# Handles: status, health, next
|
||||
|
||||
use cli/flags.nu *
|
||||
# Import all from diagnostics modules
|
||||
use domain/diagnostics/system_status.nu *
|
||||
use domain/diagnostics/health_check.nu *
|
||||
use domain/diagnostics/next_steps.nu *
|
||||
|
||||
# Main diagnostics command dispatcher
|
||||
export def handle_diagnostics_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
match $command {
|
||||
"status" => { handle_status $ops $flags }
|
||||
"health" => { handle_health $ops $flags }
|
||||
"next" => { handle_next $flags }
|
||||
"phase" => { handle_phase $flags }
|
||||
"" | "diagnostics" => { handle_status $ops $flags } # Default to status when no subcommand
|
||||
_ => {
|
||||
print $"❌ Unknown diagnostics command: ($command)"
|
||||
print ""
|
||||
print "Available diagnostics commands:"
|
||||
print " status - Show comprehensive system status"
|
||||
print " health - Run deep health validation"
|
||||
print " next - Get next steps recommendations"
|
||||
print " phase - Show current deployment phase"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Status command handler
|
||||
def handle_status [ops: string, flags: record] {
|
||||
let subcommand = ($ops | str trim)
|
||||
|
||||
if $subcommand == "json" or ($flags.out? | default "" | str trim) == "json" {
|
||||
provisioning status-json | to json
|
||||
} else {
|
||||
provisioning status
|
||||
}
|
||||
}
|
||||
|
||||
# Health command handler
|
||||
def handle_health [ops: string, flags: record] {
|
||||
let subcommand = ($ops | str trim)
|
||||
|
||||
if $subcommand == "json" or ($flags.out? | default "" | str trim) == "json" {
|
||||
provisioning health-json | to json
|
||||
} else {
|
||||
provisioning health
|
||||
}
|
||||
}
|
||||
|
||||
# Next steps command handler
|
||||
def handle_next [flags: record] {
|
||||
if ($flags.out? | default "" | str trim) == "json" {
|
||||
provisioning phase | to json
|
||||
} else {
|
||||
provisioning next
|
||||
}
|
||||
}
|
||||
|
||||
# Phase command handler
|
||||
def handle_phase [flags: record] {
|
||||
let phase_info = (provisioning phase)
|
||||
|
||||
if ($flags.out? | default "" | str trim) == "json" {
|
||||
$phase_info | to json
|
||||
} else {
|
||||
print $"(ansi cyan_bold)Current Deployment Phase(ansi reset)\n"
|
||||
print $"Phase: (ansi yellow)($phase_info.info.phase)(ansi reset)"
|
||||
print $"Description: ($phase_info.info.description)"
|
||||
print $"Progress: ($phase_info.info.step)/($phase_info.info.total_steps) steps \(($phase_info.progress_percentage)%\)"
|
||||
print $"Ready for deployment: (if $phase_info.info.ready_for_deployment { '✅ Yes' } else { '❌ No' })"
|
||||
}
|
||||
}
|
||||
146
nulib/cli/handlers/generation.nu
Normal file
146
nulib/cli/handlers/generation.nu
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Generation Command Handlers
|
||||
# Handles: generate commands (server, taskserv, cluster, infra)
|
||||
|
||||
use cli/flags.nu *
|
||||
# REMOVED: use ../../lib_provisioning * - causes circular import
|
||||
|
||||
# Helper to run module commands
|
||||
def run_module [
|
||||
args: string
|
||||
module: string
|
||||
option?: string
|
||||
--exec
|
||||
] {
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
|
||||
if $exec {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
} else {
|
||||
^$"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
}
|
||||
}
|
||||
|
||||
# Main generation command dispatcher
|
||||
export def handle_generation_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
let use_debug = if $flags.debug_mode { "-x" } else { "" }
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
|
||||
let str_out = if ($flags.outfile | is-not-empty) { $"--outfile ($flags.outfile) " } else { "" }
|
||||
let str_template = if ($flags.template | is-not-empty) { $"--template ($flags.template)" } else { "" }
|
||||
let str_select = if ($flags.select | is-not-empty) { $"--select ($flags.select)" } else { "" }
|
||||
|
||||
# Handle Infrastructure-from-Code commands (detect, complete, workflow)
|
||||
match $command {
|
||||
"detect" => {
|
||||
# Technology detection command - directly invoke module
|
||||
let detect_module = ($env.PROVISIONING | path join "core" "nulib" "provisioning detect")
|
||||
if ($detect_module | path exists) {
|
||||
let path = if ($ops | is-not-empty) { ($ops | split row " " | first) } else { "." }
|
||||
let out_fmt = if ($flags.output_format | is-not-empty) { $"--out ($flags.output_format)" } else { "" }
|
||||
let pretty = if $flags.pretty_print { "--pretty" } else { "" }
|
||||
exec nu $detect_module $path $out_fmt $pretty
|
||||
} else {
|
||||
print -e $"❌ Detection module not found at: ($detect_module)"
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
}
|
||||
"complete" => {
|
||||
# Infrastructure completion command - directly invoke module
|
||||
let complete_module = ($env.PROVISIONING | path join "core" "nulib" "provisioning complete")
|
||||
if ($complete_module | path exists) {
|
||||
let path = if ($ops | is-not-empty) { ($ops | split row " " | first) } else { "." }
|
||||
let out_fmt = if ($flags.output_format | is-not-empty) { $"--out ($flags.output_format)" } else { "" }
|
||||
let check = if $flags.check_mode { "--check" } else { "" }
|
||||
let pretty = if $flags.pretty_print { "--pretty" } else { "" }
|
||||
exec nu $complete_module $path $out_fmt $check $pretty
|
||||
} else {
|
||||
print -e $"❌ Completion module not found at: ($complete_module)"
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
}
|
||||
"workflow" => {
|
||||
# Full Infrastructure-from-Code workflow - directly invoke module
|
||||
let workflow_module = ($env.PROVISIONING | path join "core" "nulib" "provisioning workflow")
|
||||
if ($workflow_module | path exists) {
|
||||
let path = if ($ops | is-not-empty) { ($ops | split row " " | first) } else { "." }
|
||||
let org = if ($flags.org | is-not-empty) { $"--org ($flags.org)" } else { "" }
|
||||
let out_fmt = if ($flags.output_format | is-not-empty) { $"--out ($flags.output_format)" } else { "" }
|
||||
let apply = if $flags.apply_changes { "--apply" } else { "" }
|
||||
let verbose = if $flags.verbose_output { "--verbose" } else { "" }
|
||||
let pretty = if $flags.pretty_print { "--pretty" } else { "" }
|
||||
exec nu $workflow_module $path $org $out_fmt $apply $verbose $pretty
|
||||
} else {
|
||||
print -e $"❌ Workflow module not found at: ($workflow_module)"
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
}
|
||||
"orchestrate" => {
|
||||
# IaC → Orchestrator integration - directly invoke module
|
||||
let orch_module = ($env.PROVISIONING | path join "core" "nulib" "provisioning orchestrate")
|
||||
if ($orch_module | path exists) {
|
||||
let path = if ($ops | is-not-empty) { ($ops | split row " " | first) } else { "." }
|
||||
let org = if ($flags.org | is-not-empty) { $"--org ($flags.org)" } else { "" }
|
||||
let infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra)" } else { "" }
|
||||
let verbose = if $flags.verbose_output { "--verbose" } else { "" }
|
||||
let dry_run = if $flags.dry_run { "--dry-run" } else { "" }
|
||||
exec nu $orch_module $path $org $infra $verbose $dry_run
|
||||
} else {
|
||||
print -e $"❌ Orchestration module not found at: ($orch_module)"
|
||||
exit 1
|
||||
}
|
||||
return
|
||||
}
|
||||
_ => { } # Fall through to regular generate command handling
|
||||
}
|
||||
|
||||
# If no subcommand, show general generate help
|
||||
if ($ops | is-empty) {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug "generate" $ops $use_check $str_infra $str_out
|
||||
return
|
||||
}
|
||||
|
||||
let target = ($ops | split row " " | first | default null)
|
||||
let gen_ops = ($ops | split row " " | skip 1 | str join " ") + $" ($str_infra) ($str_template) ($str_out) ($use_check) ($use_debug) ($str_select)"
|
||||
|
||||
match $target {
|
||||
"s" | "server" => {
|
||||
run_module $"- ($gen_ops)" "server" "generate" --exec
|
||||
}
|
||||
"t" | "task" | "taskserv" => {
|
||||
run_module $"- ($gen_ops)" "taskserv" "generate" --exec
|
||||
}
|
||||
"i" | "infra" | "infras" => {
|
||||
run_module $"- ($gen_ops)" "infra" "generate" --exec
|
||||
}
|
||||
"cl" | "cluster" => {
|
||||
run_module $"- ($gen_ops)" "cluster" "generate" --exec
|
||||
}
|
||||
"h" | "help" => {
|
||||
_print $"\n(provisioning_generate_options)"
|
||||
exit
|
||||
}
|
||||
"new" => {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug "generate" "new" $gen_ops $str_template $use_check $str_infra $str_out
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown generate target: ($target)"
|
||||
print ""
|
||||
print "Available generate targets:"
|
||||
print " server - Generate server configurations"
|
||||
print " taskserv - Generate taskserv definitions"
|
||||
print " cluster - Generate cluster configurations"
|
||||
print " infra - Generate infrastructure setup"
|
||||
print " new - Generate new infrastructure from scratch"
|
||||
print ""
|
||||
print "Use 'provisioning generate help' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
503
nulib/cli/handlers/golden_image.nu
Normal file
503
nulib/cli/handlers/golden_image.nu
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
# Golden Image CLI Commands (Phase 3)
|
||||
#
|
||||
# User-facing commands for building, managing, and using golden images.
|
||||
|
||||
use platform/vm/golden_image_builder {
|
||||
"build-golden-image"
|
||||
"list-golden-images"
|
||||
"get-image-info"
|
||||
"delete-golden-image"
|
||||
"create-vm-from-golden-image"
|
||||
"build-image-from-vm"
|
||||
}
|
||||
|
||||
use platform/vm/golden_image_cache {
|
||||
"cache-initialize"
|
||||
"cache-add"
|
||||
"cache-get"
|
||||
"cache-list"
|
||||
"cache-cleanup"
|
||||
"cache-stats"
|
||||
"version-create"
|
||||
"version-list"
|
||||
"version-get"
|
||||
"version-deprecate"
|
||||
"version-delete"
|
||||
"version-rollback"
|
||||
}
|
||||
|
||||
export def "image build" [
|
||||
name: string # Image name
|
||||
--base-os: string = "ubuntu" # Base OS
|
||||
--os-version: string = "22.04" # OS version
|
||||
--taskservs: list<string> = [] # Taskservs to install
|
||||
--disk-size: int = 30 # Disk size in GB
|
||||
--optimize = false # Optimize image
|
||||
--no-cache = false # Don't cache result
|
||||
--check = false # Dry-run
|
||||
]: table {
|
||||
"""
|
||||
Build a golden image with pre-installed taskservs.
|
||||
|
||||
Creates a disk image with base OS + pre-installed taskservs
|
||||
for 5x faster VM startup.
|
||||
|
||||
Examples:
|
||||
# Build web server image with Nginx + PostgreSQL
|
||||
provisioning image build web-server \
|
||||
--taskservs nginx postgresql redis --disk-size 50
|
||||
|
||||
# Build Kubernetes node image
|
||||
provisioning image build k8s-node \
|
||||
--taskservs containerd kubernetes cilium --disk-size 100 --optimize
|
||||
|
||||
# Dry-run without building
|
||||
provisioning image build my-image --check
|
||||
"""
|
||||
|
||||
print "🏗️ Building golden image: '$name'"
|
||||
print " Base OS: $base_os $os_version"
|
||||
print $" Taskservs: ($taskservs | str join ', ')"
|
||||
print " Disk size: $($disk_size)GB"
|
||||
|
||||
if $check {
|
||||
print ""
|
||||
print "✓ Configuration validated"
|
||||
return []
|
||||
}
|
||||
|
||||
let result = (
|
||||
build-golden-image $name $base_os $os_version \
|
||||
--taskservs $taskservs \
|
||||
--disk-size $disk_size \
|
||||
--optimize=$optimize \
|
||||
--cache=(not $no_cache)
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print ""
|
||||
print "✅ Image built successfully"
|
||||
print $" Job ID: ($result.job_id)"
|
||||
print $" Path: ($result.image_path)"
|
||||
print $" Size: ($result.image_size_gb)GB"
|
||||
print $" Checksum: ($result.checksum)"
|
||||
} else {
|
||||
print $"❌ Build failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "image list" [
|
||||
--detailed = false # Show detailed info
|
||||
]: table {
|
||||
"""
|
||||
List all available golden images.
|
||||
|
||||
Examples:
|
||||
provisioning image list
|
||||
provisioning image list --detailed
|
||||
"""
|
||||
|
||||
let images = (list-golden-images)
|
||||
|
||||
if ($images | length) == 0 {
|
||||
print "No golden images found"
|
||||
return []
|
||||
}
|
||||
|
||||
print $"Available golden images: ($images | length)"
|
||||
print ""
|
||||
|
||||
$images
|
||||
}
|
||||
|
||||
export def "image info" [
|
||||
name: string # Image name
|
||||
]: record {
|
||||
"""
|
||||
Get detailed information about a golden image.
|
||||
|
||||
Shows size, versions, and usage information.
|
||||
|
||||
Examples:
|
||||
provisioning image info web-server
|
||||
"""
|
||||
|
||||
let info = (get-image-info $name)
|
||||
|
||||
if ("error" in $info) {
|
||||
print $"❌ Error: ($info.error)"
|
||||
return $info
|
||||
}
|
||||
|
||||
print $"Image: ($info.name)"
|
||||
print $" Path: ($info.path)"
|
||||
print $" Size: ($info.size)"
|
||||
print $" Created: ($info.created_at)"
|
||||
print $" Checksum: ($info.checksum)"
|
||||
print $" Versions: ($info.versions)"
|
||||
print $" VMs using: ($info.vms_using)"
|
||||
print ""
|
||||
|
||||
if ($info.version_list | length) > 0 {
|
||||
print " Versions:"
|
||||
$info.version_list | each {|v| print " - $v"}
|
||||
}
|
||||
|
||||
if ($info.vm_list | length) > 0 {
|
||||
print " Used by VMs:"
|
||||
$info.vm_list | each {|vm| print " - $vm"}
|
||||
}
|
||||
|
||||
$info
|
||||
}
|
||||
|
||||
export def "image delete" [
|
||||
name: string # Image name
|
||||
--force = false # Force delete if in use
|
||||
]: table {
|
||||
"""
|
||||
Delete a golden image.
|
||||
|
||||
Checks if image is in use before deletion.
|
||||
|
||||
Examples:
|
||||
provisioning image delete old-image
|
||||
provisioning image delete old-image --force
|
||||
"""
|
||||
|
||||
let result = (delete-golden-image $name --force=$force)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Image '($name)' deleted"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
if ("vms_using" in $result) {
|
||||
print $" VMs using this image: ($result.vms_using | length)"
|
||||
$result.vms_using | each {|vm| print $" - ($vm)"}
|
||||
}
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "vm create-from-image" [
|
||||
vm_name: string # VM name
|
||||
image_name: string # Golden image name
|
||||
--cpu: int = 2 # CPU cores
|
||||
--memory: int = 4096 # Memory in MB
|
||||
--disk: int = 0 # Additional disk (0=default)
|
||||
--permanent = false # Permanent VM
|
||||
--taskservs: list<string> = [] # Additional taskservs
|
||||
--check = false # Dry-run
|
||||
]: table {
|
||||
"""
|
||||
Create a VM from a golden image.
|
||||
|
||||
Much faster than building VM from scratch (~45s vs 5+ minutes).
|
||||
|
||||
Examples:
|
||||
# Create web server from image
|
||||
provisioning vm create-from-image web-01 web-server --cpu 4 --memory 8192
|
||||
|
||||
# Create Kubernetes node from image
|
||||
provisioning vm create-from-image k8s-node-01 k8s-node --permanent
|
||||
|
||||
# Dry-run
|
||||
provisioning vm create-from-image test-vm my-image --check
|
||||
"""
|
||||
|
||||
if $check {
|
||||
print "✓ VM creation validated"
|
||||
return []
|
||||
}
|
||||
|
||||
print "🚀 Creating VM from golden image..."
|
||||
print $" VM: ($vm_name)"
|
||||
print $" Image: ($image_name)"
|
||||
print " Estimated startup: ~45 seconds"
|
||||
|
||||
let result = (
|
||||
create-vm-from-golden-image $vm_name $image_name \
|
||||
--cpu=$cpu \
|
||||
--memory=$memory \
|
||||
--disk=$disk \
|
||||
--permanent=$permanent \
|
||||
--taskservs=$taskservs
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print ""
|
||||
print "✅ VM created successfully"
|
||||
print $" VM: ($result.vm_name)"
|
||||
print $" Startup time: ($result.startup_time_seconds)s"
|
||||
} else {
|
||||
print $"❌ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "image from-vm" [
|
||||
vm_name: string # VM name
|
||||
image_name: string # New image name
|
||||
--description: string = "" # Image description
|
||||
]: table {
|
||||
"""
|
||||
Create a golden image from an existing VM.
|
||||
|
||||
Useful for creating images from manually configured VMs.
|
||||
|
||||
Examples:
|
||||
provisioning image from-vm web-01 web-server --description "Web server with Nginx and PHP"
|
||||
"""
|
||||
|
||||
print "📸 Creating image from VM..."
|
||||
print $" VM: ($vm_name)"
|
||||
print $" Image: ($image_name)"
|
||||
|
||||
let result = (build-image-from-vm $vm_name $image_name --description=$description)
|
||||
|
||||
if $result.success {
|
||||
print ""
|
||||
print "✅ Image created successfully"
|
||||
print $" Image: ($result.image_name)"
|
||||
print $" Path: ($result.image_path)"
|
||||
print $" Checksum: ($result.checksum)"
|
||||
} else {
|
||||
print $"❌ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "cache init" []: record {
|
||||
"""
|
||||
Initialize golden image cache system.
|
||||
|
||||
Creates cache directories and metadata structures.
|
||||
"""
|
||||
|
||||
print "Initializing cache system..."
|
||||
let result = (cache-initialize)
|
||||
|
||||
if $result.success {
|
||||
print "✓ Cache initialized"
|
||||
print $" Directories created: ($result.cache_dirs | length)"
|
||||
}
|
||||
|
||||
$result
|
||||
}
|
||||
|
||||
export def "cache list" [
|
||||
--include-expired = false # Include expired caches
|
||||
]: table {
|
||||
"""
|
||||
List all cached images.
|
||||
|
||||
Examples:
|
||||
provisioning cache list
|
||||
provisioning cache list --include-expired
|
||||
"""
|
||||
|
||||
let caches = (cache-list --include-expired=$include_expired)
|
||||
|
||||
if ($caches | length) == 0 {
|
||||
print "No cached images"
|
||||
return []
|
||||
}
|
||||
|
||||
print $"Cached images: ($caches | length)"
|
||||
print ""
|
||||
|
||||
$caches
|
||||
}
|
||||
|
||||
export def "cache stats" []: record {
|
||||
"""
|
||||
Show cache statistics.
|
||||
|
||||
Examples:
|
||||
provisioning cache stats
|
||||
"""
|
||||
|
||||
let stats = (cache-stats)
|
||||
|
||||
print "Cache Statistics"
|
||||
print $" Total images: ($stats.total_cached_images)"
|
||||
print $" Valid caches: ($stats.valid_caches)"
|
||||
print $" Expired: ($stats.expired_caches)"
|
||||
print $" Total size: ($stats.total_size_gb)GB"
|
||||
print $" Cache hits: ($stats.total_hits)"
|
||||
print $" Total accesses: ($stats.total_accesses)"
|
||||
print $" Hit rate: ($stats.hit_rate_percent)%"
|
||||
print ""
|
||||
|
||||
$stats
|
||||
}
|
||||
|
||||
export def "cache cleanup" [
|
||||
--auto = false # Auto-cleanup expired
|
||||
--max-size-gb: int = 0 # Max cache size
|
||||
--min-free-percent: int = 10 # Min free space
|
||||
]: table {
|
||||
"""
|
||||
Cleanup expired or excess cached images.
|
||||
|
||||
Examples:
|
||||
# Auto-cleanup expired
|
||||
provisioning cache cleanup --auto
|
||||
|
||||
# Enforce size limit
|
||||
provisioning cache cleanup --max-size-gb 500
|
||||
|
||||
# Maintain minimum free space
|
||||
provisioning cache cleanup --min-free-percent 20
|
||||
"""
|
||||
|
||||
print "Cleaning up cache..."
|
||||
|
||||
let result = (
|
||||
cache-cleanup \
|
||||
--auto=$auto \
|
||||
--max-size-gb=$max_size_gb \
|
||||
--min-free-percent=$min_free_percent
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Cleaned ($result.cleaned_count) images"
|
||||
print $" Freed: ($result.cleaned_size_gb)GB"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "version create" [
|
||||
image_name: string # Image name
|
||||
version: string # Version (e.g., 1.0.0)
|
||||
image_path: string # Path to image
|
||||
--description: string = "" # Description
|
||||
--build-id: string = "" # Build job ID
|
||||
]: table {
|
||||
"""
|
||||
Create a new version of an image.
|
||||
|
||||
Examples:
|
||||
provisioning version create web-server 1.0.0 /path/to/image.qcow2 \
|
||||
--description "Initial release"
|
||||
"""
|
||||
|
||||
let result = (
|
||||
version-create $image_name $version $image_path \
|
||||
--description=$description \
|
||||
--build-id=$build_id
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Version created: ($image_name):($version)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "version list" [
|
||||
image_name: string # Image name
|
||||
]: table {
|
||||
"""
|
||||
List all versions of an image.
|
||||
|
||||
Examples:
|
||||
provisioning version list web-server
|
||||
"""
|
||||
|
||||
let versions = (version-list $image_name)
|
||||
|
||||
if ($versions | length) == 0 {
|
||||
print $"No versions found for image '($image_name)'"
|
||||
return []
|
||||
}
|
||||
|
||||
print $"Versions for '$image_name': ($versions | length)"
|
||||
print ""
|
||||
|
||||
$versions
|
||||
}
|
||||
|
||||
export def "version rollback" [
|
||||
image_name: string # Image name
|
||||
from_version: string # Current version
|
||||
to_version: string # Target version
|
||||
]: table {
|
||||
"""
|
||||
Rollback to a previous image version.
|
||||
|
||||
Examples:
|
||||
provisioning version rollback web-server 1.1.0 1.0.0
|
||||
"""
|
||||
|
||||
let result = (version-rollback $image_name $from_version $to_version)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Rolled back from ($from_version) to ($to_version)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "version deprecate" [
|
||||
image_name: string # Image name
|
||||
version: string # Version to deprecate
|
||||
--replacement: string = "" # Replacement version
|
||||
]: table {
|
||||
"""
|
||||
Mark a version as deprecated.
|
||||
|
||||
Examples:
|
||||
provisioning version deprecate web-server 1.0.0 --replacement 1.1.0
|
||||
"""
|
||||
|
||||
let result = (
|
||||
version-deprecate $image_name $version \
|
||||
--replacement=$replacement
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Version ($version) marked as deprecated"
|
||||
if ($replacement | is-empty | not) {
|
||||
print $" Replacement: ($replacement)"
|
||||
}
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "golden-image-stats" []: record {
|
||||
"""
|
||||
Show golden image system statistics.
|
||||
|
||||
Shows build history, cache status, and performance metrics.
|
||||
"""
|
||||
|
||||
let cache_stats = (cache-stats)
|
||||
|
||||
print "Golden Image System Statistics"
|
||||
print ""
|
||||
print "Cache Status:"
|
||||
print $" Total cached images: ($cache_stats.total_cached_images)"
|
||||
print $" Valid: ($cache_stats.valid_caches)"
|
||||
print $" Expired: ($cache_stats.expired_caches)"
|
||||
print $" Total size: ($cache_stats.total_size_gb)GB"
|
||||
print $" Cache hit rate: ($cache_stats.hit_rate_percent)%"
|
||||
print ""
|
||||
|
||||
{
|
||||
cache_stats: $cache_stats
|
||||
}
|
||||
}
|
||||
360
nulib/cli/handlers/guides.nu
Normal file
360
nulib/cli/handlers/guides.nu
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# Guide Command Handler
|
||||
# Provides interactive access to guides and cheatsheets
|
||||
|
||||
use primitives/io/interface.nu [_ansi _print]
|
||||
|
||||
# Display condensed cheatsheet summary
|
||||
def display_cheatsheet_summary [] {
|
||||
print ""
|
||||
print $"(_ansi cyan_bold)╔════════════════════════════════════════════════════════════════╗(_ansi reset)"
|
||||
print $"(_ansi cyan_bold)║ (_ansi yellow_bold)📋 QUICK COMMAND REFERENCE(_ansi reset) (_ansi cyan_bold)║(_ansi reset)"
|
||||
print $"(_ansi cyan_bold)╚════════════════════════════════════════════════════════════════╝(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Infrastructure:(_ansi reset)"
|
||||
print $" provisioning s list # List servers"
|
||||
print $" provisioning s create --infra <name> # Create servers [use --check for dry-run]"
|
||||
print $" provisioning t list # List task services"
|
||||
print $" provisioning t create kubernetes # Install Kubernetes"
|
||||
print $" provisioning cl list # List clusters"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Workspace:(_ansi reset)"
|
||||
print $" provisioning ws init <name> # Initialize workspace"
|
||||
print $" provisioning ws init <name> --infra <i> # Init with infrastructure"
|
||||
print $" provisioning ws init <name> --template <t># minimal, full, example"
|
||||
print $" provisioning ws list # List workspaces"
|
||||
print $" provisioning dt # Discover taskservs"
|
||||
print $" provisioning discover taskservs # Discover all taskservs"
|
||||
print $" provisioning dp # Discover providers"
|
||||
print $" provisioning dc # Discover clusters"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Templates:(_ansi reset)"
|
||||
print $" provisioning tpl list # List all templates"
|
||||
print $" provisioning tpl list --type taskservs # List taskserv templates"
|
||||
print $" provisioning tpl show <name> # Show template details"
|
||||
print $" provisioning tpl apply <name> <infra> # Apply template"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Orchestration:(_ansi reset)"
|
||||
print $" provisioning wf list # List workflows"
|
||||
print $" provisioning wf monitor <id> # Monitor workflow"
|
||||
print $" provisioning bat submit <file.ncl> # Submit batch workflow"
|
||||
print $" provisioning orch status # Orchestrator status"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Platform:(_ansi reset)"
|
||||
print $" provisioning control-center # Start web control center"
|
||||
print $" provisioning mcp-server # Start MCP server"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Configuration:(_ansi reset)"
|
||||
print $" provisioning env # Show environment"
|
||||
print $" provisioning val config # Validate configuration"
|
||||
print $" provisioning lyr show <name> # Show layer resolution"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Help & Guides:(_ansi reset)"
|
||||
print $" provisioning help # Main help"
|
||||
print $" provisioning help infra # Infrastructure help"
|
||||
print $" provisioning sc # This quick reference"
|
||||
print $" provisioning quickstart # Full command cheatsheet"
|
||||
print $" provisioning guide from-scratch # Complete deployment guide"
|
||||
print $" provisioning guide update # Update guide"
|
||||
print $" provisioning guide customize # Customization guide"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Common Flags:(_ansi reset)"
|
||||
print $" --check, -c # Dry-run mode [no changes]"
|
||||
print $" --debug, -x # Enable debug output"
|
||||
print $" --yes, -y # Auto-confirm operations"
|
||||
print $" --infra, -i # Specify infrastructure"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)Shortcuts Summary:(_ansi reset)"
|
||||
print $" (_ansi cyan_bold)Infrastructure:(_ansi reset) s=server, t=taskserv, cl=cluster, i=infra"
|
||||
print $" (_ansi cyan_bold)Workspace:(_ansi reset) ws=workspace, tpl=template"
|
||||
print $" (_ansi cyan_bold)Discovery:(_ansi reset) dt=discover taskservs, dp=providers, dc=clusters"
|
||||
print $" (_ansi cyan_bold)Orchestration:(_ansi reset) wf=workflow, bat=batch, orch=orchestrator"
|
||||
print $" (_ansi cyan_bold)Config:(_ansi reset) env, val=validate, lyr=layer"
|
||||
print $" (_ansi cyan_bold)Utils:(_ansi reset) ssh, sops, list=l/ls, nu"
|
||||
print $" (_ansi cyan_bold)Help:(_ansi reset) h=help, sc=shortcuts, guide"
|
||||
print ""
|
||||
print $"(_ansi default_dimmed)💡 Quick tips:(_ansi reset)"
|
||||
print $"(_ansi default_dimmed) • Use 'provisioning sc' to see this reference anytime(_ansi reset)"
|
||||
print $"(_ansi default_dimmed) • Use 'provisioning quickstart' for complete guide with examples(_ansi reset)"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Display markdown file with best available renderer
|
||||
def display_markdown [file: path] {
|
||||
# Check which renderers are available
|
||||
let has_glow = (which glow | length) > 0
|
||||
let has_bat = (which bat | length) > 0
|
||||
let has_mdcat = (which mdcat | length) > 0
|
||||
|
||||
# Use best available renderer (without interactive pagers - they hang with < /dev/null)
|
||||
if $has_glow {
|
||||
# glow: Beautiful markdown rendering (no --pager to avoid hanging with stdin redirected)
|
||||
^glow $file
|
||||
} else if $has_bat {
|
||||
# bat: Good syntax highlighting (no --pager to avoid hanging)
|
||||
^bat $file --style=auto --language=markdown
|
||||
} else if $has_mdcat {
|
||||
# mdcat: Markdown-specific renderer (no piping to less to avoid hanging)
|
||||
^mdcat $file
|
||||
} else {
|
||||
# Fallback: Direct print with manual pagination
|
||||
let content = (open $file)
|
||||
print $content
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)💡 TIP: Install a markdown renderer for better viewing(_ansi reset)"
|
||||
print ""
|
||||
print $" (_ansi green)glow(_ansi reset) [recommended]: brew install glow (_ansi default_dimmed)# macOS(_ansi reset)"
|
||||
print $" apt install glow (_ansi default_dimmed)# Ubuntu/Debian(_ansi reset)"
|
||||
print $" dnf install glow (_ansi default_dimmed)# Fedora(_ansi reset)"
|
||||
print ""
|
||||
print $" (_ansi green)bat(_ansi reset): brew install bat (_ansi default_dimmed)# macOS(_ansi reset)"
|
||||
print $" apt install bat (_ansi default_dimmed)# Ubuntu/Debian(_ansi reset)"
|
||||
print ""
|
||||
}
|
||||
}
|
||||
|
||||
# Display markdown with optional URL information
|
||||
def display_markdown_with_url [file: path, doc_path: string] {
|
||||
# Show URL if configured
|
||||
let url_info = (resolve-doc-url $doc_path)
|
||||
if ($url_info.mode == "url") and ($url_info.url != null) {
|
||||
print $"📖 (_ansi cyan)Documentation: ($url_info.url)(_ansi reset)"
|
||||
print $"📁 (_ansi cyan_bold)Local file: ($url_info.local)(_ansi reset)"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Display guide with formatting
|
||||
display_markdown $file
|
||||
}
|
||||
|
||||
# Main guide command dispatcher
|
||||
export def handle_guide_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
match $command {
|
||||
"guide" | "guides" => { handle_guide $ops $flags }
|
||||
"sc" | "shortcuts" => { display_cheatsheet_summary }
|
||||
"quickstart" | "quick" => { guide_quickstart }
|
||||
"from-scratch" | "scratch" => { guide_from_scratch }
|
||||
"update" | "upgrade" => { guide_update }
|
||||
"customize" | "custom" => { guide_customize }
|
||||
"howto" => { guide_list }
|
||||
_ => {
|
||||
print $"❌ Unknown guide command: ($command)"
|
||||
print ""
|
||||
print "Available guide commands:"
|
||||
print " guide [topic] - Show specific guide (list, quickstart, from-scratch, update, customize)"
|
||||
print " sc | shortcuts - Quick command reference (FASTEST)"
|
||||
print " quickstart | quick - Full command cheatsheet"
|
||||
print " from-scratch | scratch - Step-by-step deployment guide"
|
||||
print " update | upgrade - Update existing infrastructure"
|
||||
print " customize | custom - Customize with layers and templates"
|
||||
print " howto - List all guides"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning sc"
|
||||
print " provisioning quickstart"
|
||||
print " provisioning from-scratch"
|
||||
print " provisioning guide update"
|
||||
print ""
|
||||
print "Use 'provisioning help guides' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Guide handler
|
||||
def handle_guide [ops: string, flags: record] {
|
||||
let topic = if ($ops | is-empty) { "list" } else { $ops }
|
||||
|
||||
match $topic {
|
||||
"list" => { guide_list }
|
||||
"sc" => { display_cheatsheet_summary }
|
||||
"quickstart" | "shortcuts" | "quick" => { guide_quickstart }
|
||||
"from-scratch" | "scratch" | "start" | "deploy" => { guide_from_scratch }
|
||||
"update" | "upgrade" => { guide_update }
|
||||
"customize" | "custom" | "layers" | "templates" => { guide_customize }
|
||||
_ => {
|
||||
print $"❌ Unknown guide topic: ($topic)"
|
||||
print ""
|
||||
guide_list
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List available guides
|
||||
def guide_list [] {
|
||||
print ""
|
||||
print $"(_ansi cyan_bold)📚 AVAILABLE GUIDES(_ansi reset)"
|
||||
print ""
|
||||
print $" (_ansi green_bold)sc(_ansi reset) - Quick command reference (_ansi yellow_bold)[FASTEST](_ansi reset)"
|
||||
print $" (_ansi green)quickstart(_ansi reset) - Full command cheatsheet with examples"
|
||||
print $" (_ansi green)from-scratch(_ansi reset) - Step-by-step guide from init to deploy"
|
||||
print $" (_ansi green)update(_ansi reset) - Update existing infrastructure"
|
||||
print $" (_ansi green)customize(_ansi reset) - Customize infrastructure with layers"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)SHORTCUTS:(_ansi reset)"
|
||||
print $" • (_ansi cyan_bold)sc, shortcuts(_ansi reset) → Quick reference (_ansi default_dimmed)fastest, no pager(_ansi reset)"
|
||||
print $" • (_ansi cyan)quickstart(_ansi reset) → quick"
|
||||
print $" • (_ansi cyan)from-scratch(_ansi reset) → scratch, start, deploy"
|
||||
print $" • (_ansi cyan)update(_ansi reset) → upgrade"
|
||||
print $" • (_ansi cyan)customize(_ansi reset) → custom, layers, templates"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)USAGE:(_ansi reset)"
|
||||
print $" provisioning sc - Quick command reference (_ansi yellow)fastest(_ansi reset)"
|
||||
print $" provisioning shortcuts - Same as sc"
|
||||
print $" provisioning guide quickstart - Full command reference"
|
||||
print $" provisioning guide from-scratch - Complete deployment guide"
|
||||
print $" provisioning guide update - Infrastructure update guide"
|
||||
print $" provisioning guide customize - Customization guide"
|
||||
print ""
|
||||
print $"(_ansi cyan_bold)💡 BETTER VIEWING EXPERIENCE(_ansi reset)"
|
||||
print ""
|
||||
print " For beautifully formatted guides, install a markdown renderer:"
|
||||
print ""
|
||||
print $" (_ansi green)glow(_ansi reset) [recommended] - Beautiful markdown rendering"
|
||||
print $" macOS: (_ansi cyan)brew install glow(_ansi reset)"
|
||||
print $" Ubuntu/Debian: (_ansi cyan)apt install glow(_ansi reset)"
|
||||
print $" Fedora: (_ansi cyan)dnf install glow(_ansi reset)"
|
||||
print $" Other: (_ansi cyan)go install github.com/charmbracelet/glow@latest(_ansi reset)"
|
||||
print ""
|
||||
print $" (_ansi green)bat(_ansi reset) - Syntax highlighting"
|
||||
print $" macOS: (_ansi cyan)brew install bat(_ansi reset)"
|
||||
print $" Ubuntu/Debian: (_ansi cyan)apt install bat(_ansi reset)"
|
||||
print ""
|
||||
print $" (_ansi default_dimmed)Without a renderer, guides display as plain text with pagination.(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)💡 All guides provide copy-paste ready commands(_ansi reset)"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Display quickstart cheatsheet
|
||||
def guide_quickstart [] {
|
||||
let guide_file = "provisioning/docs/src/guides/quickstart-cheatsheet.md"
|
||||
|
||||
if not ($guide_file | path exists) {
|
||||
print $"❌ Guide file not found: ($guide_file)"
|
||||
print $" Expected location: ($env.PWD)/($guide_file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Display guide with formatting
|
||||
print ""
|
||||
print ""
|
||||
print $"(_ansi cyan_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print $"(_ansi cyan_bold) (_ansi yellow_bold)📚 QUICK COMMAND CHEATSHEET(_ansi reset)"
|
||||
print $"(_ansi cyan_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print ""
|
||||
|
||||
# Display guide with URL information and markdown rendering
|
||||
display_markdown_with_url $guide_file "guides/quickstart-cheatsheet"
|
||||
|
||||
print ""
|
||||
print $"(_ansi green_bold)✅ Cheatsheet displayed(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow)Other guides:(_ansi reset)"
|
||||
print $" • provisioning guide from-scratch"
|
||||
print $" • provisioning guide update"
|
||||
print $" • provisioning guide customize"
|
||||
|
||||
# Show cheatsheet summary
|
||||
display_cheatsheet_summary
|
||||
}
|
||||
|
||||
# Display from-scratch guide
|
||||
def guide_from_scratch [] {
|
||||
let guide_file = "provisioning/docs/src/guides/from-scratch.md"
|
||||
|
||||
if not ($guide_file | path exists) {
|
||||
print $"❌ Guide file not found: ($guide_file)"
|
||||
print $" Expected location: ($env.PWD)/($guide_file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Display guide with formatting
|
||||
print ""
|
||||
print $"(_ansi green_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print $"(_ansi green_bold)║ (_ansi yellow_bold)🚀 FROM SCRATCH: COMPLETE INFRASTRUCTURE DEPLOYMENT(_ansi reset) (_ansi green_bold)║(_ansi reset)"
|
||||
print $"(_ansi green_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print ""
|
||||
|
||||
# Display guide with URL information and markdown rendering
|
||||
display_markdown_with_url $guide_file "guides/from-scratch"
|
||||
|
||||
print ""
|
||||
print $"(_ansi green_bold)✅ Guide displayed(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow)Other guides:(_ansi reset)"
|
||||
print $" • provisioning guide quickstart"
|
||||
print $" • provisioning guide update"
|
||||
print $" • provisioning guide customize"
|
||||
|
||||
# Show cheatsheet summary
|
||||
display_cheatsheet_summary
|
||||
}
|
||||
|
||||
# Display update guide
|
||||
def guide_update [] {
|
||||
let guide_file = "provisioning/docs/src/guides/update-infrastructure.md"
|
||||
|
||||
if not ($guide_file | path exists) {
|
||||
print $"❌ Guide file not found: ($guide_file)"
|
||||
print $" Expected location: ($env.PWD)/($guide_file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Display guide with formatting
|
||||
print ""
|
||||
print $"(_ansi blue_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print $"(_ansi blue_bold)║ (_ansi yellow_bold)🔄 UPDATE EXISTING INFRASTRUCTURE(_ansi reset) (_ansi blue_bold)║(_ansi reset)"
|
||||
print $"(_ansi blue_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print ""
|
||||
|
||||
# Display guide with URL information and markdown rendering
|
||||
display_markdown_with_url $guide_file "guides/update-infrastructure"
|
||||
|
||||
print ""
|
||||
print $"(_ansi green_bold)✅ Guide displayed(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow)Other guides:(_ansi reset)"
|
||||
print $" • provisioning guide quickstart"
|
||||
print $" • provisioning guide from-scratch"
|
||||
print $" • provisioning guide customize"
|
||||
|
||||
# Show cheatsheet summary
|
||||
display_cheatsheet_summary
|
||||
}
|
||||
|
||||
# Display customize guide
|
||||
def guide_customize [] {
|
||||
let guide_file = "provisioning/docs/src/guides/customize-infrastructure.md"
|
||||
|
||||
if not ($guide_file | path exists) {
|
||||
print $"❌ Guide file not found: ($guide_file)"
|
||||
print $" Expected location: ($env.PWD)/($guide_file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Display guide with formatting
|
||||
print ""
|
||||
print $"(_ansi purple_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print $"(_ansi purple_bold)║ (_ansi yellow_bold)🎨 CUSTOMIZE INFRASTRUCTURE(_ansi reset) (_ansi purple_bold)║(_ansi reset)"
|
||||
print $"(_ansi purple_bold)═══════════════════════════════════════════════════════════════(_ansi reset)"
|
||||
print ""
|
||||
|
||||
# Display guide with URL information and markdown rendering
|
||||
display_markdown_with_url $guide_file "guides/customize-infrastructure"
|
||||
|
||||
print ""
|
||||
print $"(_ansi green_bold)✅ Guide displayed(_ansi reset)"
|
||||
print ""
|
||||
print $"(_ansi yellow)Other guides:(_ansi reset)"
|
||||
print $" • provisioning guide quickstart"
|
||||
print $" • provisioning guide from-scratch"
|
||||
print $" • provisioning guide update"
|
||||
|
||||
# Show cheatsheet summary
|
||||
display_cheatsheet_summary
|
||||
}
|
||||
673
nulib/cli/handlers/infrastructure.nu
Normal file
673
nulib/cli/handlers/infrastructure.nu
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
# Infrastructure Command Handlers
|
||||
# Handles: server, taskserv, cluster, infra commands
|
||||
|
||||
use cli/flags.nu *
|
||||
# REMOVED: use ../../lib_provisioning * - causes circular import
|
||||
use platform/plugins/auth.nu *
|
||||
|
||||
# Pre-load server module to preserve plugin context (tera, auth, kms, etc.)
|
||||
# This is needed so template rendering and other plugin operations work
|
||||
# in the same Nushell process
|
||||
use orchestration/servers/create.nu *
|
||||
|
||||
# Helper to run module commands
|
||||
# Modules are pre-loaded above to preserve plugin context
|
||||
def run_module [
|
||||
args: string
|
||||
module: string
|
||||
subcommand?: string # Optional explicit subcommand (for create operations)
|
||||
--exec
|
||||
] {
|
||||
# Convert args string to list by splitting on spaces
|
||||
let args_list = if ($args | is-not-empty) {
|
||||
$args | split row " " | where {|x| ($x | str trim | is-not-empty) }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Call the appropriate module's main function
|
||||
# Server module is pre-loaded above, so plugins (tera, auth, kms, etc.) are in scope
|
||||
match $module {
|
||||
"server" => {
|
||||
# For server: call the "main create" function directly from the already-loaded servers/create.nu
|
||||
# This preserves the tera plugin context in the same process
|
||||
# If subcommand is explicitly provided (from handle_server), use it
|
||||
# Otherwise, extract from args
|
||||
let actual_subcommand = if ($subcommand | is-not-empty) {
|
||||
$subcommand
|
||||
} else {
|
||||
let op_list = ($args | split row " " | where { |x| ($x | is-not-empty) })
|
||||
if ($op_list | length) > 0 { $op_list | first } else { "help" }
|
||||
}
|
||||
|
||||
# For now, only handle "create" directly. For others, use -mod
|
||||
match $actual_subcommand {
|
||||
"create" | "c" | "list" | "l" => {
|
||||
# The servers/create.nu and servers/list.nu are loaded modules
|
||||
# Call "main create" or "main list" function directly with the arguments
|
||||
# This preserves context (env vars, plugins, etc.) in the same process
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let cmd_args = [-mod, "server", $actual_subcommand, ...$args_list]
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug ...$cmd_args
|
||||
}
|
||||
_ => {
|
||||
# For other operations (delete, ssh, price, status, etc.), use -mod with explicit subcommand
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let cmd_args = [-mod, "server", $actual_subcommand, ...$args_list]
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug ...$cmd_args
|
||||
}
|
||||
}
|
||||
}
|
||||
"taskserv" | "task" => {
|
||||
# Taskserv uses exec mode
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let cmd_args = [-mod, $module, ...$args_list, --notitles]
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug ...$cmd_args
|
||||
}
|
||||
"cluster" => {
|
||||
# Cluster uses exec mode
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let cmd_args = [-mod, $module, ...$args_list, --notitles]
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug ...$cmd_args
|
||||
}
|
||||
"infra" => {
|
||||
# Infra uses exec mode since it's a legacy module
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
let cmd_args = [-mod, $module, ...$args_list, --notitles]
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug ...$cmd_args
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown module: ($module)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Show infrastructure commands help
|
||||
def show_infrastructure_help [] {
|
||||
print ""
|
||||
print "INFRASTRUCTURE"
|
||||
print ""
|
||||
print " s server Server lifecycle — create, delete, list, ssh, price"
|
||||
print " t taskserv L2 provisioning — create, update, reset, delete, status"
|
||||
print " list → components filtered to mode=taskserv"
|
||||
print " show → component show"
|
||||
print " c component Unified component catalog and workspace instances"
|
||||
print " e component (ext) list [--mode taskserv|cluster|container] [--workspace <ws>]"
|
||||
print " show <name> [--workspace <ws>] [--ext]"
|
||||
print " status <name> --workspace <ws>"
|
||||
print " vm Virtual machine management"
|
||||
print ""
|
||||
print "ORCHESTRATION"
|
||||
print ""
|
||||
print " w workflow WorkflowDef lifecycle — list, show, run, validate, status"
|
||||
print " j job Orchestrator job management — list, status, monitor, submit"
|
||||
print " b batch Batch operations"
|
||||
print " o orchestrator Orchestrator daemon lifecycle"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " prvng c list # all components"
|
||||
print " prvng c list --mode cluster # cluster-mode only"
|
||||
print " prvng c show postgresql --workspace libre-daoshi # full component view"
|
||||
print " prvng c status k0s --workspace libre-daoshi # FSM state only"
|
||||
print " prvng w list --workspace libre-daoshi # workspace workflows"
|
||||
print " prvng w run deploy-services-libre-daoshi --workspace libre-daoshi"
|
||||
print " prvng t create --infra libre-daoshi # L2 provisioning"
|
||||
print " prvng s list # server list"
|
||||
print " prvng j list # orchestrator jobs"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Main infrastructure command dispatcher
|
||||
export def handle_infrastructure_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
set_debug_env $flags
|
||||
|
||||
match $command {
|
||||
"create" | "c" => {
|
||||
# Handle: provisioning create server/taskserv/cluster <name> ...
|
||||
let create_ops_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where {|x| ($x | is-not-empty) }
|
||||
} else { [] }
|
||||
|
||||
let resource_type = if (($create_ops_list | length) > 0) {
|
||||
$create_ops_list | first
|
||||
} else { "" }
|
||||
|
||||
let resource_name_and_args = if (($create_ops_list | length) > 1) {
|
||||
$create_ops_list | skip 1 | str join " "
|
||||
} else { "" }
|
||||
|
||||
|
||||
match $resource_type {
|
||||
"server" | "s" => {
|
||||
let server_args = $"create ($resource_name_and_args)"
|
||||
handle_server $server_args $flags
|
||||
}
|
||||
"taskserv" | "task" | "t" => {
|
||||
let taskserv_args = $"create ($resource_name_and_args)"
|
||||
handle_taskserv $taskserv_args $flags
|
||||
}
|
||||
"cluster" | "cl" => {
|
||||
let cluster_args = $"create ($resource_name_and_args)"
|
||||
handle_cluster $cluster_args $flags
|
||||
}
|
||||
_ => {
|
||||
if ($resource_type | is-empty) {
|
||||
print "❌ Resource type required for create command"
|
||||
} else {
|
||||
print $"❌ Unknown resource type for create: ($resource_type)"
|
||||
}
|
||||
print ""
|
||||
print "Usage: provisioning create <resource> <name>"
|
||||
print ""
|
||||
print "Resources:"
|
||||
print " server (s) - Create a server"
|
||||
print " taskserv (t) - Create a task service"
|
||||
print " cluster (cl) - Create a cluster"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
"delete" | "d" => {
|
||||
# Handle: provisioning delete server/taskserv/cluster <name> ...
|
||||
let delete_ops_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where {|x| ($x | is-not-empty) }
|
||||
} else { [] }
|
||||
|
||||
let resource_type = if (($delete_ops_list | length) > 0) {
|
||||
$delete_ops_list | first
|
||||
} else { "" }
|
||||
|
||||
let resource_name_and_args = if (($delete_ops_list | length) > 1) {
|
||||
$delete_ops_list | skip 1 | str join " "
|
||||
} else { "" }
|
||||
|
||||
match $resource_type {
|
||||
"server" | "s" => {
|
||||
let server_args = $"delete ($resource_name_and_args)"
|
||||
handle_server $server_args $flags
|
||||
}
|
||||
"taskserv" | "task" | "t" => {
|
||||
let taskserv_args = $"delete ($resource_name_and_args)"
|
||||
handle_taskserv $taskserv_args $flags
|
||||
}
|
||||
"cluster" | "cl" => {
|
||||
let cluster_args = $"delete ($resource_name_and_args)"
|
||||
handle_cluster $cluster_args $flags
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown resource type for delete: ($resource_type)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
"bootstrap" | "bstrap" => { handle_bootstrap $ops $flags }
|
||||
"fip" | "floating-ip" => { handle_fip $ops $flags }
|
||||
"server" => { handle_server $ops $flags }
|
||||
"taskserv" | "task" => { handle_taskserv $ops $flags }
|
||||
"component" | "comp" => { handle_component $ops $flags }
|
||||
"extension" | "ext" => { handle_extension $ops $flags }
|
||||
"cluster" => { handle_component $ops $flags } # cluster → component (deprecated alias)
|
||||
"vm" => {
|
||||
# Import VM domain handler
|
||||
use vm_domain.nu handle_vm_command
|
||||
|
||||
# Parse VM subcommand
|
||||
let vm_ops_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where {|x| ($x | is-not-empty) }
|
||||
} else { [] }
|
||||
|
||||
let vm_command = if (($vm_ops_list | length) > 0) {
|
||||
$vm_ops_list | first
|
||||
} else { "vm" }
|
||||
|
||||
let vm_remaining_ops = if (($vm_ops_list | length) > 1) {
|
||||
$vm_ops_list | skip 1 | str join " "
|
||||
} else { "" }
|
||||
|
||||
handle_vm_command $vm_command $vm_remaining_ops $flags
|
||||
}
|
||||
"infra" | "infras" => {
|
||||
# Show help if no ops provided
|
||||
if ($ops | is-empty) {
|
||||
show_infrastructure_help
|
||||
} else {
|
||||
handle_infra $ops $flags
|
||||
}
|
||||
}
|
||||
"infrastructure" | "help" | "" => { show_infrastructure_help }
|
||||
_ => {
|
||||
print $"❌ Unknown command: ($command)"
|
||||
show_infrastructure_help
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Floating IP command handler
|
||||
def handle_fip [ops: string, flags: record] {
|
||||
use ../../orchestration/fip.nu *
|
||||
|
||||
let ops_list = if ($ops | is-not-empty) {
|
||||
$ops | split row " " | where {|x| ($x | is-not-empty) }
|
||||
} else { [] }
|
||||
|
||||
let subcommand = if ($ops_list | length) > 0 { $ops_list | first } else { "" }
|
||||
let remaining = if ($ops_list | length) > 1 { $ops_list | skip 1 } else { [] }
|
||||
let out_flag = ($flags | get --optional output_format | default "")
|
||||
|
||||
match $subcommand {
|
||||
"list" | "l" => {
|
||||
if ($out_flag | is-not-empty) { main list --out $out_flag } else { main list }
|
||||
}
|
||||
"show" | "s" => {
|
||||
let name = if ($remaining | length) > 0 { $remaining | first } else {
|
||||
error make { msg: "Usage: provisioning fip show <name>" }
|
||||
}
|
||||
if ($out_flag | is-not-empty) { main show $name --out $out_flag } else { main show $name }
|
||||
}
|
||||
"assign" => {
|
||||
let name = if ($remaining | length) > 0 { $remaining | get 0 } else {
|
||||
error make { msg: "Usage: provisioning fip assign <name> <server>" }
|
||||
}
|
||||
let server = if ($remaining | length) > 1 { $remaining | get 1 } else {
|
||||
error make { msg: "Usage: provisioning fip assign <name> <server>" }
|
||||
}
|
||||
let yes = $flags.auto_confirm
|
||||
main assign $name $server --yes=$yes
|
||||
}
|
||||
"unassign" => {
|
||||
let name = if ($remaining | length) > 0 { $remaining | first } else {
|
||||
error make { msg: "Usage: provisioning fip unassign <name>" }
|
||||
}
|
||||
let yes = $flags.auto_confirm
|
||||
main unassign $name --yes=$yes
|
||||
}
|
||||
"protection" => {
|
||||
let name = if ($remaining | length) > 0 { $remaining | get 0 } else {
|
||||
error make { msg: "Usage: provisioning fip protection <name> <enable|disable>" }
|
||||
}
|
||||
let action = if ($remaining | length) > 1 { $remaining | get 1 } else {
|
||||
error make { msg: "Usage: provisioning fip protection <name> <enable|disable>" }
|
||||
}
|
||||
main protection $name $action
|
||||
}
|
||||
_ => {
|
||||
print "Floating IP Management"
|
||||
print "====================="
|
||||
print ""
|
||||
print "Usage: provisioning fip <command> [args]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " list List all Floating IPs with role and protection"
|
||||
print " show <name> Show detail for a specific FIP"
|
||||
print " assign <name> <server> Assign FIP to a server"
|
||||
print " unassign <name> Release FIP from its current server"
|
||||
print " protection <name> enable|disable Toggle delete protection"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning fip list"
|
||||
print " provisioning fip show librecloud-fip-smtp"
|
||||
print " provisioning fip assign librecloud-fip-smtp sgoyol-1"
|
||||
print " provisioning fip unassign librecloud-fip-smtp"
|
||||
print " provisioning fip protection librecloud-fip-smtp enable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Bootstrap command handler — L1 Hetzner resource provisioning
|
||||
def handle_bootstrap [ops: string, flags: record] {
|
||||
use ../../orchestration/bootstrap.nu *
|
||||
let ws = ($flags | get --optional workspace | default "")
|
||||
let dry = $flags.dry_run
|
||||
if ($ws | is-not-empty) {
|
||||
main bootstrap --workspace $ws --dry-run=$dry
|
||||
} else {
|
||||
main bootstrap --dry-run=$dry
|
||||
}
|
||||
}
|
||||
|
||||
# Server command handler
|
||||
def handle_server [ops: string, flags: record] {
|
||||
# Show help if no subcommand provided
|
||||
if ($ops | is-empty) {
|
||||
print "Server Management"
|
||||
print "================="
|
||||
print ""
|
||||
print "Usage: provisioning server <command> [options]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " create <name> Create a new server"
|
||||
print " delete <name> Delete a server"
|
||||
print " list List all servers"
|
||||
print " ssh <name> SSH into server"
|
||||
print " price Show server pricing"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning server create web-01"
|
||||
print " provisioning server list"
|
||||
print " provisioning server ssh web-01"
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
# Authentication check for server operations (metadata-driven)
|
||||
let operation_parts = ($ops | split row " " | where {|x| ($x | is-not-empty)})
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Determine operation type
|
||||
let operation_type = match $action {
|
||||
"create" | "c" => "create"
|
||||
"delete" | "d" | "remove" => "delete"
|
||||
"modify" | "update" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
# Check authentication using metadata-driven approach
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"server ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
# Extract the remaining arguments after the action verb (create/delete/list/etc)
|
||||
let action_and_args = if ($operation_parts | length) > 1 {
|
||||
$operation_parts | skip 1 | str join " "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
let args = build_module_args $flags $action_and_args
|
||||
# Pass the action as explicit subcommand so run_module knows which operation is being performed
|
||||
# For create operations, this preserves plugin context by calling "main create" directly
|
||||
run_module $args "server" $action --exec
|
||||
}
|
||||
|
||||
# Task service command handler
|
||||
def handle_taskserv [ops: string, flags: record] {
|
||||
# Show help if no subcommand provided
|
||||
if ($ops | is-empty) {
|
||||
print "Task Service Management"
|
||||
print "======================"
|
||||
print ""
|
||||
print "Usage: provisioning taskserv <command> [options]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " create <service> Create a task service"
|
||||
print " delete <service> Delete a task service"
|
||||
print " list List all task services"
|
||||
print " generate <service> Generate task service config"
|
||||
print ""
|
||||
print "Service Mesh Options:"
|
||||
print " istio - Full-featured service mesh with built-in ingress gateway"
|
||||
print " linkerd - Lightweight service mesh (requires external ingress)"
|
||||
print " cilium - CNI with service mesh capabilities"
|
||||
print ""
|
||||
print "Ingress Controller Options:"
|
||||
print " nginx-ingress - Most popular, battle-tested ingress controller"
|
||||
print " traefik - Modern cloud-native ingress with middleware"
|
||||
print " contour - Envoy-based ingress with simple configuration"
|
||||
print " haproxy-ingress - High-performance HAProxy-based ingress"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning taskserv create kubernetes"
|
||||
print " provisioning taskserv create istio"
|
||||
print " provisioning taskserv create linkerd"
|
||||
print " provisioning taskserv create nginx-ingress"
|
||||
print " provisioning taskserv create traefik"
|
||||
print " provisioning taskserv list"
|
||||
print ""
|
||||
print "Recommended Combinations:"
|
||||
print " 1. Linkerd + Nginx Ingress - Lightweight mesh + proven ingress"
|
||||
print " 2. Istio (standalone) - Full-featured with built-in gateway"
|
||||
print " 3. Linkerd + Traefik - Lightweight mesh + modern ingress"
|
||||
print " 4. No mesh + Nginx Ingress - Simple deployments"
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
# Authentication check for taskserv operations (metadata-driven)
|
||||
let operation_parts = ($ops | split row " ")
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Determine operation type
|
||||
let operation_type = match $action {
|
||||
"create" | "c" => "create"
|
||||
"delete" | "d" | "remove" => "delete"
|
||||
"modify" | "update" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
# Check authentication using metadata-driven approach
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"taskserv ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
# Show ontoref FSM state from both ontology instances:
|
||||
# 1. provisioning project domain ($PROVISIONING/.ontology/)
|
||||
# 2. active workspace domain ($PROVISIONING_KLOUD_PATH/.ontology/)
|
||||
let ontoref_bin = (do { ^which ontoref } | complete | get stdout | str trim)
|
||||
if ($ontoref_bin | is-not-empty) {
|
||||
let prov_path = ($env.PROVISIONING? | default "")
|
||||
let kloud_path = ($env.PROVISIONING_KLOUD_PATH? | default "")
|
||||
let onto_roots = (
|
||||
[$prov_path, $kloud_path]
|
||||
| where { |p| ($p | is-not-empty) and ($p | path join ".ontology" "state.ncl" | path exists) }
|
||||
| uniq
|
||||
)
|
||||
if ($onto_roots | is-not-empty) {
|
||||
print ""
|
||||
for root in $onto_roots {
|
||||
do { cd $root; ^ontoref describe state } | complete | get stdout | print
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "taskserv" --exec
|
||||
}
|
||||
|
||||
# Cluster command handler
|
||||
def handle_cluster [ops: string, flags: record] {
|
||||
# Show help if no subcommand provided
|
||||
if ($ops | is-empty) {
|
||||
print "Cluster Management"
|
||||
print "=================="
|
||||
print ""
|
||||
print "Usage: provisioning cluster <command> [options]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " deploy <layer> <cluster> Deploy L3 platform or L4 app extensions"
|
||||
print " create <name> Create a new cluster"
|
||||
print " delete <name> Delete a cluster"
|
||||
print " list List all clusters"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning cluster deploy platform sgoyol --ws librecloud_renew"
|
||||
print " provisioning cluster deploy apps sgoyol --ws librecloud_renew"
|
||||
print " provisioning cluster create k8s-prod"
|
||||
print " provisioning cluster list"
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
let operation_parts = ($ops | split row " " | where { $in | is-not-empty })
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Intercept deploy — routes to cluster-deploy.nu, not the old -mod cluster module
|
||||
if $action in ["deploy"] {
|
||||
use ../../orchestration/cluster_deploy.nu *
|
||||
let rest = ($operation_parts | skip 1)
|
||||
let layer = ($rest | get -o 0 | default "")
|
||||
let cluster = ($rest | get -o 1 | default "")
|
||||
if ($layer | is-empty) or ($cluster | is-empty) {
|
||||
print "❌ Usage: provisioning cluster deploy <layer> <cluster> [--ws <workspace>]"
|
||||
print " layer: platform | apps"
|
||||
exit 1
|
||||
}
|
||||
let ws = ($flags | get --optional workspace | default "")
|
||||
let dry = $flags.dry_run
|
||||
let kube_cfg = ""
|
||||
let sec_file = ""
|
||||
if ($ws | is-not-empty) {
|
||||
main cluster deploy $layer $cluster --workspace $ws --dry-run=$dry --kubeconfig $kube_cfg --secrets-file $sec_file
|
||||
} else {
|
||||
main cluster deploy $layer $cluster --dry-run=$dry --kubeconfig $kube_cfg --secrets-file $sec_file
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Determine operation type for auth check
|
||||
let operation_type = match $action {
|
||||
"create" | "c" => "create"
|
||||
"delete" | "d" | "remove" | "destroy" => "delete"
|
||||
"modify" | "update" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"cluster ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "cluster" --exec
|
||||
}
|
||||
|
||||
# Infrastructure command handler
|
||||
def handle_infra [ops: string, flags: record] {
|
||||
# Handle infra-specific argument building
|
||||
let infra_arg = if ($flags.infra | is-not-empty) {
|
||||
$"-i ($flags.infra)"
|
||||
} else if ($flags.infras | is-not-empty) {
|
||||
$"--infras ($flags.infras)"
|
||||
} else {
|
||||
$"-i (get_infra | path basename)"
|
||||
}
|
||||
|
||||
let use_yes = if $flags.auto_confirm { "--yes" } else { "" }
|
||||
let use_check = if $flags.check_mode { "--check" } else { "" }
|
||||
let use_onsel = if ($flags.onsel | is-not-empty) {
|
||||
$"--onsel ($flags.onsel)"
|
||||
} else { "" }
|
||||
|
||||
let args = $"($ops) ($infra_arg) ($use_check) ($use_onsel) ($use_yes)" | str trim
|
||||
run_module $args "infra"
|
||||
}
|
||||
|
||||
# Price/cost command handler
|
||||
export def handle_price_command [ops: string, flags: record] {
|
||||
let use_check = if $flags.check_mode { "--check " } else { "" }
|
||||
let str_infra = if ($flags.infra | is-not-empty) {
|
||||
$"--infra ($flags.infra) "
|
||||
} else { "" }
|
||||
let str_out = if ($flags.outfile | is-not-empty) {
|
||||
$"--outfile ($flags.outfile) "
|
||||
} else { "" }
|
||||
|
||||
run_module $"($ops) ($str_infra) ($use_check) ($str_out)" "server" "price" --exec
|
||||
}
|
||||
|
||||
# Create-server-task combined command handler
|
||||
export def handle_create_server_task [ops: string, flags: record] {
|
||||
# Create servers first
|
||||
let server_args = build_module_args $flags $ops
|
||||
run_module $server_args "server" "create"
|
||||
|
||||
# Check if server creation succeeded
|
||||
if $env.LAST_EXIT_CODE != 0 {
|
||||
_print $"🛑 Errors found in (_ansi yellow_bold)create-server(_ansi reset)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create taskservs
|
||||
let taskserv_args = build_module_args $flags $"- ($ops)"
|
||||
run_module $taskserv_args "taskserv" "create"
|
||||
}
|
||||
|
||||
# Component command handler — unified view for catalog/components
|
||||
def handle_component [ops: string, flags: record] {
|
||||
let parts = ($ops | split row " ")
|
||||
let action = if ($parts | is-empty) { "" } else { $parts | first }
|
||||
let workspace = ($flags.workspace? | default ($flags.ws? | default ""))
|
||||
let mode = ($flags.mode? | default "")
|
||||
|
||||
# BROKEN: file not found — use ../../components/mod.nu *
|
||||
|
||||
match $action {
|
||||
"list" | "ls" | "l" | "" => {
|
||||
component-list $mode $workspace
|
||||
}
|
||||
"show" | "s" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: component show requires a name"
|
||||
return
|
||||
}
|
||||
let name = ($parts | get 1)
|
||||
let ext_only = ($flags.ext? | default false)
|
||||
component-show $name $workspace $ext_only
|
||||
}
|
||||
"status" | "st" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: component status requires a name"
|
||||
return
|
||||
}
|
||||
let name = ($parts | get 1)
|
||||
component-status $name $workspace
|
||||
}
|
||||
"" => {
|
||||
print "Component Management"
|
||||
print "===================="
|
||||
print ""
|
||||
print "Usage: provisioning component <command> [options]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " list [--mode taskserv|cluster|container] [--workspace <ws>]"
|
||||
print " show <name> [--workspace <ws>] [--ext]"
|
||||
print " status <name> [--workspace <ws>]"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning component list"
|
||||
print " provisioning component list --mode cluster"
|
||||
print " provisioning component show postgresql --workspace libre-daoshi"
|
||||
print " provisioning component status k0s --workspace libre-daoshi"
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown component subcommand: ($action)"
|
||||
print "Use 'provisioning component' for help"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Extension command handler — browses extension catalog (catalog/components/ definitions)
|
||||
# e / ext → extension → shows metadata, modes, requires/provides without workspace context
|
||||
def handle_extension [ops: string, flags: record] {
|
||||
let parts = ($ops | split row " ")
|
||||
let action = if ($parts | is-empty) { "" } else { $parts | first }
|
||||
let mode = ($flags.mode? | default "")
|
||||
|
||||
# BROKEN: file not found — use ../../components/mod.nu *
|
||||
|
||||
match $action {
|
||||
"list" | "ls" | "l" | "" => {
|
||||
# Extension catalog: no workspace filter (ext_only view)
|
||||
component-list $mode ""
|
||||
}
|
||||
"show" | "s" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: extension show requires a name"
|
||||
return
|
||||
}
|
||||
component-show ($parts | get 1) "" true # ext_only = true
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown extension subcommand: ($action)"
|
||||
print "Use: prvng e list | prvng e show <name>"
|
||||
}
|
||||
}
|
||||
}
|
||||
289
nulib/cli/handlers/integrations/auth.nu
Normal file
289
nulib/cli/handlers/integrations/auth.nu
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
# Authentication Command Handler
|
||||
# Domain: JWT authentication with system keyring integration
|
||||
# Plugin: nu_plugin_auth integration with HTTP fallback
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
# Login - uses plugin if available, HTTP fallback otherwise
|
||||
def auth-login [
|
||||
username: string
|
||||
password?: string
|
||||
--url: string = ""
|
||||
--save = false
|
||||
--check = false
|
||||
] {
|
||||
if $check {
|
||||
return { action: "login", user: $username, mode: "dry-run" }
|
||||
}
|
||||
|
||||
let use_url = if ($url | is-empty) { "http://localhost:8081" } else { $url }
|
||||
|
||||
if (is-plugin-available "nu_plugin_auth") {
|
||||
# Use native plugin (10x faster)
|
||||
{ success: true, user: $username, token: "plugin-token", source: "plugin" }
|
||||
} else {
|
||||
# HTTP fallback
|
||||
let body = { username: $username, password: ($password | default "") }
|
||||
{ success: true, user: $username, token: "http-fallback-token", source: "http" }
|
||||
}
|
||||
}
|
||||
|
||||
# Logout - uses plugin if available
|
||||
def auth-logout [--url: string = "", --check = false] {
|
||||
if $check {
|
||||
return { action: "logout", mode: "dry-run" }
|
||||
}
|
||||
|
||||
if (is-plugin-available "nu_plugin_auth") {
|
||||
{ success: true, message: "Logged out (plugin mode)" }
|
||||
} else {
|
||||
{ success: true, message: "Logged out (no plugin)" }
|
||||
}
|
||||
}
|
||||
|
||||
# Verify token - uses plugin if available
|
||||
def auth-verify [--local = false, --url: string = ""] {
|
||||
if (is-plugin-available "nu_plugin_auth") {
|
||||
# Plugin available - call it directly without --local flag for now (fallback below)
|
||||
{ valid: true, token: "verified", source: "plugin" }
|
||||
} else {
|
||||
# HTTP fallback
|
||||
{ valid: true, token: "verified", source: "http" }
|
||||
}
|
||||
}
|
||||
|
||||
# List sessions - uses plugin if available
|
||||
def auth-sessions [--active = false] {
|
||||
if (is-plugin-available "nu_plugin_auth") {
|
||||
[]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# FLOW=CONTINUE EXAMPLE: auth-integrate with TTY_OUTPUT
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# This function demonstrates the flow=continue pattern:
|
||||
# 1. TTY wrapper (auth-integrate-tty.sh) prompts user for credentials
|
||||
# 2. Wrapper outputs JSON to stdout
|
||||
# 3. Filter captures output in $TTY_OUTPUT environment variable
|
||||
# 4. Nushell script (this function) receives both CLI args AND TTY output
|
||||
# 5. Script processes credentials and CLI args together
|
||||
#
|
||||
# Usage: provisioning auth integrate --provider <name> [--save]
|
||||
# Example: provisioning auth integrate --provider azure --save
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Integrate provider credentials (uses flow=continue TTY input)
|
||||
def auth-integrate [
|
||||
--provider: string = ""
|
||||
--save = false
|
||||
--check = false
|
||||
] {
|
||||
# Guard 1: Provider specified
|
||||
if ($provider | is-empty) {
|
||||
error make {msg: "Provider required: --provider <name>"}
|
||||
}
|
||||
|
||||
if $check {
|
||||
return { action: "integrate", provider: $provider, mode: "dry-run" }
|
||||
}
|
||||
|
||||
# Guard 2: Check if TTY wrapper was executed (flow=continue case)
|
||||
# $env.TTY_OUTPUT contains credentials from the bash wrapper
|
||||
let tty_output = ($env.TTY_OUTPUT? | default "")
|
||||
|
||||
# If no TTY output, credentials weren't provided via TTY
|
||||
if ($tty_output | is-empty) {
|
||||
error make {msg: "No credentials provided via TTY input"}
|
||||
}
|
||||
|
||||
# Parse credentials from TTY output (JSON format from auth-integrate-tty.sh)
|
||||
# Validate JSON structure first
|
||||
if not ($tty_output | str starts-with '{') {
|
||||
error make {msg: "Invalid credentials format: not JSON"}
|
||||
}
|
||||
|
||||
let credentials = $tty_output | from json
|
||||
|
||||
# Guard 3: Validate credentials structure
|
||||
if not ($credentials | get username? | is-not-empty) {
|
||||
error make {msg: "Credentials missing 'username'"}
|
||||
}
|
||||
|
||||
if not ($credentials | get password? | is-not-empty) {
|
||||
error make {msg: "Credentials missing 'password'"}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Integration Logic: Use both TTY credentials AND CLI provider argument
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
let username = $credentials.username
|
||||
let password = $credentials.password
|
||||
let timestamp = ($credentials.timestamp? | default (date now | format date '%Y-%m-%dT%H:%M:%SZ'))
|
||||
|
||||
# Perform provider-specific integration
|
||||
let result = match $provider {
|
||||
"azure" => {
|
||||
# Azure integration with credentials
|
||||
{
|
||||
provider: "azure"
|
||||
status: "integrated"
|
||||
username: $username
|
||||
timestamp: $timestamp
|
||||
keyring_stored: $save
|
||||
message: "Azure credentials integrated successfully"
|
||||
}
|
||||
}
|
||||
"aws" => {
|
||||
# AWS integration with credentials
|
||||
{
|
||||
provider: "aws"
|
||||
status: "integrated"
|
||||
username: $username
|
||||
timestamp: $timestamp
|
||||
keyring_stored: $save
|
||||
message: "AWS credentials integrated successfully"
|
||||
}
|
||||
}
|
||||
"gcp" => {
|
||||
# GCP integration with credentials
|
||||
{
|
||||
provider: "gcp"
|
||||
status: "integrated"
|
||||
username: $username
|
||||
timestamp: $timestamp
|
||||
keyring_stored: $save
|
||||
message: "GCP credentials integrated successfully"
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error make {msg: $"Unknown provider: ($provider)"}
|
||||
}
|
||||
}
|
||||
|
||||
# If --save flag set, store credentials in keyring
|
||||
if $save {
|
||||
# TODO: Store credentials in system keyring
|
||||
# This would use nu_plugin_kms or similar
|
||||
}
|
||||
|
||||
# Clear sensitive data from environment (security: hide credentials)
|
||||
hide-env TTY_OUTPUT
|
||||
|
||||
# Return integration result
|
||||
$result
|
||||
}
|
||||
|
||||
# Auth command handler
|
||||
export def cmd-auth [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) {
|
||||
help-auth
|
||||
return
|
||||
}
|
||||
|
||||
match $action {
|
||||
"login" => {
|
||||
let username = ($args | get 0?)
|
||||
if ($username == null) {
|
||||
print "Usage: provisioning auth login <username> [password]"
|
||||
exit 1
|
||||
}
|
||||
let password = ($args | get 1?)
|
||||
let result = (auth-login $username $password --check=$check)
|
||||
if $check {
|
||||
print $"Would login as: ($username)"
|
||||
} else {
|
||||
print "Login successful"
|
||||
print $result
|
||||
}
|
||||
}
|
||||
"logout" => {
|
||||
let result = (auth-logout --check=$check)
|
||||
print $result.message
|
||||
}
|
||||
"verify" => {
|
||||
let local = ("--local" in $args) or ("-l" in $args)
|
||||
let result = (auth-verify --local=$local)
|
||||
if $result.valid? == true {
|
||||
print "Token is valid"
|
||||
print $result
|
||||
} else {
|
||||
print $"Token verification failed: ($result.error? | default 'unknown')"
|
||||
}
|
||||
}
|
||||
"sessions" => {
|
||||
let active = ("--active" in $args)
|
||||
let sessions = (auth-sessions --active=$active)
|
||||
if ($sessions | length) == 0 {
|
||||
print "No active sessions"
|
||||
} else {
|
||||
print "Active sessions:"
|
||||
$sessions | table
|
||||
}
|
||||
}
|
||||
"integrate" => {
|
||||
# Extract provider from args or from CLI
|
||||
let provider = ($args | get 0?)
|
||||
|
||||
# Guard: Provider must be specified
|
||||
if ($provider | is-empty) {
|
||||
error make {msg: "Provider not specified"}
|
||||
}
|
||||
|
||||
# Execute integration (auth-integrate handles its own error handling)
|
||||
let result = (auth-integrate --provider=$provider --check=$check)
|
||||
if $check {
|
||||
print $"Would integrate provider: ($provider)"
|
||||
} else {
|
||||
print $"Provider ($provider) integrated successfully"
|
||||
print $result
|
||||
}
|
||||
}
|
||||
"status" => {
|
||||
let plugin_status = (plugins-status)
|
||||
print "Authentication Plugin Status:"
|
||||
print $" Plugin installed: ($plugin_status.auth)"
|
||||
print $" Mode: (if $plugin_status.auth { 'Native plugin \(10x faster\)' } else { 'HTTP fallback' })"
|
||||
}
|
||||
"help" | "--help" => { help-auth }
|
||||
_ => {
|
||||
print $"Unknown auth command: [$action]"
|
||||
help-auth
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Help for authentication commands
|
||||
def help-auth [] {
|
||||
print "Authentication - JWT auth with system keyring integration"
|
||||
print ""
|
||||
print "Usage: provisioning auth <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " login <user> [pass] Authenticate user (stores token in keyring)"
|
||||
print " logout End session and remove stored token"
|
||||
print " verify Verify current token validity"
|
||||
print " sessions List active sessions"
|
||||
print " integrate --provider Integrate provider credentials via TTY (flow=continue)"
|
||||
print " status Show plugin status"
|
||||
print ""
|
||||
print "Performance: 10x faster with nu_plugin_auth vs HTTP fallback"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning auth login admin"
|
||||
print " provisioning auth verify --local"
|
||||
print " provisioning auth sessions --active"
|
||||
print " provisioning auth integrate --provider azure --save"
|
||||
print ""
|
||||
print "⚡ TTY Input Flow:"
|
||||
print " The 'integrate' action uses flow=continue (TTY input → Nushell processing)"
|
||||
print " User credentials are captured in bash wrapper, passed to Nushell script"
|
||||
}
|
||||
93
nulib/cli/handlers/integrations/backup.nu
Normal file
93
nulib/cli/handlers/integrations/backup.nu
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Backup Command Handler
|
||||
# Domain: Multi-backend backup management (restic, borg, tar, rsync)
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
def backup-create [name: string paths: list --check = false] { {name: $name, paths: $paths} }
|
||||
def backup-restore [snapshot_id: string --check = false] { {snapshot_id: $snapshot_id} }
|
||||
def backup-list [--backend = "restic"] { [] }
|
||||
def backup-schedule [name: string cron: string] { {name: $name, cron: $cron} }
|
||||
def backup-retention [] { {daily: 7, weekly: 4, monthly: 12, yearly: 7} }
|
||||
def backup-status [job_id: string] { {job_id: $job_id, status: "pending"} }
|
||||
|
||||
export def cmd-backup [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-backup; return }
|
||||
|
||||
match $action {
|
||||
"create" => {
|
||||
let name = ($args | get 0?)
|
||||
if ($name == null) {
|
||||
print "Usage: provisioning backup create <name> [paths...]"
|
||||
exit 1
|
||||
}
|
||||
let paths = ($args | skip 1)
|
||||
let result = (backup-create $name $paths --check=$check)
|
||||
print $"Backup created: [$result.name]"
|
||||
}
|
||||
"restore" => {
|
||||
let snapshot_id = ($args | get 0?)
|
||||
if ($snapshot_id == null) {
|
||||
print "Usage: provisioning backup restore <snapshot_id>"
|
||||
exit 1
|
||||
}
|
||||
let result = (backup-restore $snapshot_id --check=$check)
|
||||
print $"Restore initiated: [$result.snapshot_id]"
|
||||
}
|
||||
"list" => {
|
||||
let backend = ($args | get 0? | default "restic")
|
||||
let snapshots = (backup-list --backend=$backend)
|
||||
if ($snapshots | length) == 0 {
|
||||
print "No snapshots found"
|
||||
} else {
|
||||
print "Available snapshots:"
|
||||
}
|
||||
}
|
||||
"schedule" => {
|
||||
let name = ($args | get 0?)
|
||||
let cron = ($args | get 1?)
|
||||
if ($name == null or $cron == null) {
|
||||
print "Usage: provisioning backup schedule <name> <cron>"
|
||||
exit 1
|
||||
}
|
||||
let result = (backup-schedule $name $cron)
|
||||
print $"Schedule created: [$result.name]"
|
||||
}
|
||||
"retention" => {
|
||||
let config = (backup-retention)
|
||||
print $"Retention policy:"
|
||||
print $" Daily: [$config.daily] days"
|
||||
print $" Weekly: [$config.weekly] weeks"
|
||||
print $" Monthly: [$config.monthly] months"
|
||||
print $" Yearly: [$config.yearly] years"
|
||||
}
|
||||
"status" => {
|
||||
let job_id = ($args | get 0?)
|
||||
if ($job_id == null) {
|
||||
print "Usage: provisioning backup status <job_id>"
|
||||
exit 1
|
||||
}
|
||||
let status = (backup-status $job_id)
|
||||
print $"Job [$status.job_id]: ($status.status)"
|
||||
}
|
||||
"help" | "--help" => { help-backup }
|
||||
_ => { print $"Unknown backup command: [$action]"; help-backup; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
def help-backup [] {
|
||||
print "Backup management - Multi-backend backup with retention"
|
||||
print ""
|
||||
print "Usage: provisioning backup <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " create <name> [paths] Create backup job"
|
||||
print " restore <snapshot> Restore from snapshot"
|
||||
print " list [backend] List snapshots"
|
||||
print " schedule <name> <cron> Schedule regular backups"
|
||||
print " retention Show retention policy"
|
||||
print " status <job_id> Check backup status"
|
||||
}
|
||||
84
nulib/cli/handlers/integrations/gitops.nu
Normal file
84
nulib/cli/handlers/integrations/gitops.nu
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# GitOps Command Handler
|
||||
# Domain: Event-driven deployments from Git repositories
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
def gitops-rules [config_path: string] { [] }
|
||||
def gitops-watch [--provider = "github"] { {provider: $provider, webhook_port: 9000} }
|
||||
def gitops-trigger [rule: string --check = false] { {rule: $rule, deployment_id: "dep-123"} }
|
||||
def gitops-event-types [] { ["push" "pull_request" "tag"] }
|
||||
def gitops-deployments [--status: string = ""] { [] }
|
||||
def gitops-status [] { {active_rules: 0, total_deployments: 0} }
|
||||
|
||||
export def cmd-gitops [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-gitops; return }
|
||||
|
||||
match $action {
|
||||
"rules" => {
|
||||
let config_path = ($args | get 0?)
|
||||
if ($config_path == null) {
|
||||
print "Usage: provisioning gitops rules <config_file>"
|
||||
exit 1
|
||||
}
|
||||
let rules = (gitops-rules $config_path)
|
||||
print $"Loaded ($rules | length) GitOps rules"
|
||||
}
|
||||
"watch" => {
|
||||
let provider = ($args | get 0? | default "github")
|
||||
print $"Watching for events on [$provider]..."
|
||||
if (not $check) {
|
||||
let result = (gitops-watch --provider=$provider)
|
||||
print $"Webhook listening on port [$result.webhook_port]"
|
||||
}
|
||||
}
|
||||
"trigger" => {
|
||||
let rule = ($args | get 0?)
|
||||
if ($rule == null) {
|
||||
print "Usage: provisioning gitops trigger <rule_name>"
|
||||
exit 1
|
||||
}
|
||||
let result = (gitops-trigger $rule --check=$check)
|
||||
print $"Deployment triggered: [$result.deployment_id]"
|
||||
}
|
||||
"events" => {
|
||||
let events = (gitops-event-types)
|
||||
print "Supported events:"
|
||||
$events | each {|e| print $" • $e"}
|
||||
}
|
||||
"deployments" => {
|
||||
let status_filter = ($args | get 0?)
|
||||
let deployments = (gitops-deployments --status=$status_filter)
|
||||
if ($deployments | length) == 0 {
|
||||
print "No deployments found"
|
||||
} else {
|
||||
print "Active deployments:"
|
||||
}
|
||||
}
|
||||
"status" => {
|
||||
let status = (gitops-status)
|
||||
print "GitOps Status:"
|
||||
print $" Active Rules: [$status.active_rules]"
|
||||
print $" Total Deployments: [$status.total_deployments]"
|
||||
}
|
||||
"help" | "--help" => { help-gitops }
|
||||
_ => { print $"Unknown gitops command: [$action]"; help-gitops; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
def help-gitops [] {
|
||||
print "GitOps - Event-driven deployments from Git"
|
||||
print ""
|
||||
print "Usage: provisioning gitops <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " rules <config> Load GitOps rules"
|
||||
print " watch [provider] Watch for Git events"
|
||||
print " trigger <rule> Trigger deployment"
|
||||
print " events List supported events"
|
||||
print " deployments [status] List deployments"
|
||||
print " status Show GitOps status"
|
||||
}
|
||||
168
nulib/cli/handlers/integrations/kms.nu
Normal file
168
nulib/cli/handlers/integrations/kms.nu
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# KMS Command Handler
|
||||
# Domain: Multi-backend Key Management System
|
||||
# Plugin: nu_plugin_kms integration with HTTP fallback
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
# Encrypt data - uses plugin if available
|
||||
def kms-encrypt [
|
||||
data: string
|
||||
--backend: string = ""
|
||||
--key: string = ""
|
||||
--check = false
|
||||
] {
|
||||
if $check {
|
||||
return $"Would encrypt data with backend: ($backend | default 'auto')"
|
||||
}
|
||||
|
||||
if (is-plugin-available "nu_plugin_kms") {
|
||||
# Plugin available - use native fast encryption
|
||||
$"encrypted:($data | str length):plugin"
|
||||
} else {
|
||||
# HTTP fallback (simplified - returns mock encrypted data)
|
||||
$"encrypted:($data | str length):http"
|
||||
}
|
||||
}
|
||||
|
||||
# Decrypt data - uses plugin if available
|
||||
def kms-decrypt [
|
||||
encrypted: string
|
||||
--backend: string = ""
|
||||
--key: string = ""
|
||||
] {
|
||||
if (is-plugin-available "nu_plugin_kms") {
|
||||
# Plugin available - use native fast decryption
|
||||
$"decrypted:plugin"
|
||||
} else {
|
||||
# HTTP fallback
|
||||
$"decrypted:http"
|
||||
}
|
||||
}
|
||||
|
||||
# KMS status - uses plugin if available
|
||||
def kms-status [] {
|
||||
if (is-plugin-available "nu_plugin_kms") {
|
||||
{ backend: "rustyvault", available: true, config: "plugin-mode" }
|
||||
} else {
|
||||
{ backend: "http_fallback", available: true, config: "using HTTP API" }
|
||||
}
|
||||
}
|
||||
|
||||
# List KMS backends - uses plugin if available
|
||||
def kms-list-backends [] {
|
||||
if (is-plugin-available "nu_plugin_kms") {
|
||||
[
|
||||
{ name: "rustyvault", description: "RustyVault Transit", available: true }
|
||||
{ name: "age", description: "Age encryption", available: true }
|
||||
{ name: "aws", description: "AWS KMS", available: true }
|
||||
{ name: "vault", description: "HashiCorp Vault", available: true }
|
||||
{ name: "cosmian", description: "Cosmian encryption", available: true }
|
||||
]
|
||||
} else {
|
||||
[
|
||||
{ name: "rustyvault", description: "RustyVault Transit", available: false }
|
||||
{ name: "age", description: "Age encryption", available: true }
|
||||
{ name: "aws", description: "AWS KMS", available: false }
|
||||
{ name: "vault", description: "HashiCorp Vault", available: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# KMS command handler
|
||||
export def cmd-kms [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) {
|
||||
help-kms
|
||||
return
|
||||
}
|
||||
|
||||
match $action {
|
||||
"encrypt" => {
|
||||
let data = ($args | get 0?)
|
||||
if ($data == null) {
|
||||
print "Usage: provisioning kms encrypt <data> [--backend <backend>] [--key <key>]"
|
||||
exit 1
|
||||
}
|
||||
# Parse --backend and --key flags
|
||||
let backend = (parse-flag $args "--backend" "-b")
|
||||
let key = (parse-flag $args "--key" "-k")
|
||||
|
||||
let result = (kms-encrypt $data --backend=($backend | default "") --key=($key | default "") --check=$check)
|
||||
if $check {
|
||||
print $result
|
||||
} else {
|
||||
print "Encrypted:"
|
||||
print $result
|
||||
}
|
||||
}
|
||||
"decrypt" => {
|
||||
let encrypted = ($args | get 0?)
|
||||
if ($encrypted == null) {
|
||||
print "Usage: provisioning kms decrypt <encrypted_data> [--backend <backend>] [--key <key>]"
|
||||
exit 1
|
||||
}
|
||||
let backend = (parse-flag $args "--backend" "-b")
|
||||
let key = (parse-flag $args "--key" "-k")
|
||||
|
||||
let result = (kms-decrypt $encrypted --backend=($backend | default "") --key=($key | default ""))
|
||||
print "Decrypted:"
|
||||
print $result
|
||||
}
|
||||
"generate-key" | "genkey" => {
|
||||
print "Key generation requires direct plugin access"
|
||||
print "Use: kms generate-key --spec AES256"
|
||||
}
|
||||
"status" => {
|
||||
let status = (kms-status)
|
||||
print "KMS Status:"
|
||||
print $" Backend: ($status.backend)"
|
||||
print $" Available: ($status.available)"
|
||||
print $" Config: ($status.config)"
|
||||
}
|
||||
"list-backends" | "backends" => {
|
||||
let backends = (kms-list-backends)
|
||||
print "Available KMS Backends:"
|
||||
for backend in $backends {
|
||||
let status = if $backend.available { "[OK]" } else { "[--]" }
|
||||
print $" ($status) ($backend.name): ($backend.description)"
|
||||
}
|
||||
}
|
||||
"help" | "--help" => { help-kms }
|
||||
_ => {
|
||||
print $"Unknown kms command: [$action]"
|
||||
help-kms
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Help for KMS commands
|
||||
def help-kms [] {
|
||||
print "KMS - Multi-backend Key Management System"
|
||||
print ""
|
||||
print "Usage: provisioning kms <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " encrypt <data> Encrypt data"
|
||||
print " decrypt <encrypted> Decrypt data"
|
||||
print " generate-key Generate encryption key"
|
||||
print " status Show KMS backend status"
|
||||
print " list-backends List available backends"
|
||||
print ""
|
||||
print "Backends:"
|
||||
print " rustyvault RustyVault Transit (primary)"
|
||||
print " age Age file-based encryption"
|
||||
print " aws AWS Key Management Service"
|
||||
print " vault HashiCorp Vault Transit"
|
||||
print " cosmian Cosmian privacy-preserving"
|
||||
print ""
|
||||
print "Performance: 10x faster with nu_plugin_kms vs HTTP fallback"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning kms encrypt \"secret\" --backend age"
|
||||
print " provisioning kms decrypt \$encrypted --backend age"
|
||||
print " provisioning kms status"
|
||||
}
|
||||
150
nulib/cli/handlers/integrations/mod.nu
Normal file
150
nulib/cli/handlers/integrations/mod.nu
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# Integrations Command Dispatcher
|
||||
# Routes integration commands to appropriate domain-specific handlers
|
||||
# Provides access to prov-ecosystem, provctl, and native plugin functionality
|
||||
# NUSHELL 0.109 COMPLIANT - All handlers properly exported
|
||||
|
||||
use ./auth.nu *
|
||||
use ./kms.nu *
|
||||
use ./orch.nu *
|
||||
use ./runtime.nu *
|
||||
use ./ssh.nu *
|
||||
use ./backup.nu *
|
||||
use ./gitops.nu *
|
||||
use ./service.nu *
|
||||
use ./shared.nu *
|
||||
|
||||
# Main integration command dispatcher
|
||||
export def cmd-integrations [
|
||||
subcommand: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
match $subcommand {
|
||||
# Plugin-powered commands (10-30x faster)
|
||||
"auth" => { cmd-auth ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"kms" => { cmd-kms ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"orch" | "orchestrator" => { cmd-orch ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"plugin" | "plugins" => { cmd-plugin-status ($args | get 0?) ($args | skip 1) }
|
||||
|
||||
# Legacy integration commands
|
||||
"runtime" => { cmd-runtime ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"ssh" => { cmd-ssh ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"backup" => { cmd-backup ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"gitops" => { cmd-gitops ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"service" => { cmd-service ($args | get 0?) ($args | skip 1) --check=$check }
|
||||
"help" | "--help" | "-h" => { help-integrations }
|
||||
_ => {
|
||||
print $"Unknown integration command: [$subcommand]"
|
||||
help-integrations
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Plugin status command handler
|
||||
def cmd-plugin-status [
|
||||
action: string
|
||||
args: list = []
|
||||
] {
|
||||
if ($action == null or $action == "status") {
|
||||
let status = (plugins-status)
|
||||
print ""
|
||||
print "Provisioning Plugins Status"
|
||||
print "============================"
|
||||
print ""
|
||||
let auth_status = if $status.auth { "[OK] " } else { "[--]" }
|
||||
let kms_status = if $status.kms { "[OK] " } else { "[--]" }
|
||||
let orch_status = if $status.orchestrator { "[OK] " } else { "[--]" }
|
||||
|
||||
print $"($auth_status) nu_plugin_auth - JWT authentication with keyring"
|
||||
print $"($kms_status) nu_plugin_kms - Multi-backend encryption"
|
||||
print $"($orch_status) nu_plugin_orchestrator - Local orchestrator \(30x faster\)"
|
||||
print ""
|
||||
|
||||
let all_loaded = $status.auth and $status.kms and $status.orchestrator
|
||||
if $all_loaded {
|
||||
print "All plugins loaded - using native high-performance mode"
|
||||
} else {
|
||||
print "Some plugins not loaded - using HTTP fallback"
|
||||
print ""
|
||||
print "Install plugins with:"
|
||||
print " nu provisioning/core/plugins/install-plugins.nu"
|
||||
}
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
match $action {
|
||||
"list" => {
|
||||
let plugins = (plugin list | default [])
|
||||
let provisioning_plugins = ($plugins | where name =~ "nu_plugin_(auth|kms|orchestrator)" | default [])
|
||||
if ($provisioning_plugins | length) == 0 {
|
||||
print "No provisioning plugins registered"
|
||||
} else {
|
||||
print "Registered provisioning plugins:"
|
||||
$provisioning_plugins | table
|
||||
}
|
||||
}
|
||||
"test" => {
|
||||
print "Running plugin tests..."
|
||||
let status = (plugins-status)
|
||||
|
||||
let results = (
|
||||
[
|
||||
{ name: "auth", available: $status.auth }
|
||||
{ name: "kms", available: $status.kms }
|
||||
{ name: "orchestrator", available: $status.orchestrator }
|
||||
]
|
||||
| each { |item|
|
||||
if $item.available {
|
||||
print $" [OK] ($item.name) plugin responding"
|
||||
{ status: "ok", name: $item.name }
|
||||
} else {
|
||||
print $" [FAIL] ($item.name) plugin not available"
|
||||
{ status: "fail", name: $item.name }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let passed = ($results | where status == "ok" | length)
|
||||
let failed = ($results | where status == "fail" | length)
|
||||
|
||||
print ""
|
||||
print $"Results: ($passed) passed, ($failed) failed"
|
||||
}
|
||||
"help" | "--help" => {
|
||||
print "Plugin management commands"
|
||||
print ""
|
||||
print "Usage: provisioning plugin <action>"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " status Show plugin status (default)"
|
||||
print " list List registered plugins"
|
||||
print " test Test plugin functionality"
|
||||
}
|
||||
_ => { print $"Unknown plugin command: [$action]" }
|
||||
}
|
||||
}
|
||||
|
||||
# Help for integration commands
|
||||
def help-integrations [] {
|
||||
print "Integration commands - Access prov-ecosystem, provctl, and plugin functionality"
|
||||
print ""
|
||||
print "Usage: provisioning integrations <command> [options]"
|
||||
print ""
|
||||
print "PLUGIN-POWERED COMMANDS (10-30x faster):"
|
||||
print " auth JWT authentication with system keyring"
|
||||
print " kms Multi-backend encryption (RustyVault, Age, AWS, Vault)"
|
||||
print " orch Local orchestrator operations (30x faster than HTTP)"
|
||||
print " plugin Plugin status and management"
|
||||
print ""
|
||||
print "LEGACY INTEGRATION COMMANDS:"
|
||||
print " runtime Container runtime abstraction (docker, podman, orbstack, colima, nerdctl)"
|
||||
print " ssh Advanced SSH operations with pooling and circuit breaker"
|
||||
print " backup Multi-backend backup management (restic, borg, tar, rsync)"
|
||||
print " gitops Event-driven deployments from Git"
|
||||
print " service Cross-platform service management (systemd, launchd, runit, openrc)"
|
||||
print ""
|
||||
print "Shortcuts: int, integ, integrations"
|
||||
print "Use: provisioning <command> help"
|
||||
}
|
||||
162
nulib/cli/handlers/integrations/orch.nu
Normal file
162
nulib/cli/handlers/integrations/orch.nu
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# Orchestrator Command Handler
|
||||
# Domain: Local orchestrator operations with workflow management
|
||||
# Plugin: nu_plugin_orchestrator integration (30x faster than HTTP)
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
# Orchestrator status - uses plugin if available (30x faster)
|
||||
def orch-status [--data-dir: string = ""] {
|
||||
if (is-plugin-available "nu_plugin_orchestrator") {
|
||||
{ running: true, tasks_pending: 0, tasks_running: 0, tasks_completed: 0, mode: "plugin" }
|
||||
} else {
|
||||
{ running: true, tasks_pending: 0, tasks_running: 0, tasks_completed: 0, mode: "http" }
|
||||
}
|
||||
}
|
||||
|
||||
# List tasks - uses plugin if available
|
||||
def orch-tasks [
|
||||
--status: string = ""
|
||||
--limit: int = 100
|
||||
--data-dir: string = ""
|
||||
] {
|
||||
if (is-plugin-available "nu_plugin_orchestrator") { [] } else { [] }
|
||||
}
|
||||
|
||||
# Validate workflow - uses plugin if available
|
||||
def orch-validate [
|
||||
workflow: path
|
||||
--strict = false
|
||||
] {
|
||||
if (is-plugin-available "nu_plugin_orchestrator") {
|
||||
{ valid: true, errors: [], warnings: [], mode: "plugin" }
|
||||
} else {
|
||||
if not ($workflow | path exists) {
|
||||
return { valid: false, errors: ["Workflow file not found"], warnings: [] }
|
||||
}
|
||||
{ valid: true, errors: [], warnings: ["Plugin unavailable - basic validation only"] }
|
||||
}
|
||||
}
|
||||
|
||||
# Submit workflow - uses plugin if available
|
||||
def orch-submit [
|
||||
workflow: path
|
||||
--priority: int = 50
|
||||
--check = false
|
||||
] {
|
||||
if $check {
|
||||
return { success: true, submitted: false, message: "Dry-run mode" }
|
||||
}
|
||||
|
||||
if (is-plugin-available "nu_plugin_orchestrator") {
|
||||
{ success: true, submitted: true, task_id: "task-plugin-1", mode: "plugin" }
|
||||
} else {
|
||||
{ success: true, submitted: true, task_id: "task-http-1", mode: "http" }
|
||||
}
|
||||
}
|
||||
|
||||
# Monitor task - uses plugin if available
|
||||
def orch-monitor [
|
||||
task_id: string
|
||||
--once = false
|
||||
--interval: int = 1000
|
||||
--timeout: int = 300
|
||||
] {
|
||||
if (is-plugin-available "nu_plugin_orchestrator") {
|
||||
{ id: $task_id, status: "completed", message: "Task completed (plugin mode)", mode: "plugin" }
|
||||
} else {
|
||||
{ id: $task_id, status: "completed", message: "Task completed (http mode)", mode: "http" }
|
||||
}
|
||||
}
|
||||
|
||||
# Orchestrator command handler
|
||||
export def cmd-orch [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-orch; return }
|
||||
|
||||
match $action {
|
||||
"status" => {
|
||||
let data_dir = (parse-flag $args "--data-dir" "-d")
|
||||
let status = (orch-status --data-dir=($data_dir | default ""))
|
||||
print "Orchestrator Status:"
|
||||
print $" Running: ($status.running? | default false)"
|
||||
print $" Pending tasks: ($status.tasks_pending? | default 0)"
|
||||
print $" Running tasks: ($status.tasks_running? | default 0)"
|
||||
print $" Completed tasks: ($status.tasks_completed? | default 0)"
|
||||
}
|
||||
"tasks" => {
|
||||
let status_filter = (parse-flag $args "--status" "-s")
|
||||
let limit = (parse-flag $args "--limit" "-l" | default "100" | into int)
|
||||
let tasks = (orch-tasks --status=($status_filter | default "") --limit=$limit)
|
||||
if ($tasks | length) == 0 {
|
||||
print "No tasks found"
|
||||
} else {
|
||||
print $"Tasks \(($tasks | length)\):"
|
||||
$tasks | table
|
||||
}
|
||||
}
|
||||
"validate" => {
|
||||
let workflow = ($args | get 0?)
|
||||
if ($workflow == null) {
|
||||
print "Usage: provisioning orch validate <workflow.ncl> [--strict]"
|
||||
exit 1
|
||||
}
|
||||
let strict = ("--strict" in $args) or ("-s" in $args)
|
||||
let result = (orch-validate $workflow --strict=$strict)
|
||||
if $result.valid {
|
||||
print "Workflow is valid"
|
||||
} else {
|
||||
print "Validation failed:"
|
||||
for error in $result.errors {
|
||||
print $" - ($error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"submit" => {
|
||||
let workflow = ($args | get 0?)
|
||||
if ($workflow == null) {
|
||||
print "Usage: provisioning orch submit <workflow.ncl> [--priority <0-100>]"
|
||||
exit 1
|
||||
}
|
||||
let priority = (parse-flag $args "--priority" "-p" | default "50" | into int)
|
||||
let result = (orch-submit $workflow --priority=$priority --check=$check)
|
||||
if $result.submitted? == true {
|
||||
print $"Workflow submitted: ($result.task_id?)"
|
||||
} else {
|
||||
print $"Submission failed: ($result.error? | default $result.message?)"
|
||||
}
|
||||
}
|
||||
"monitor" => {
|
||||
let task_id = ($args | get 0?)
|
||||
if ($task_id == null) {
|
||||
print "Usage: provisioning orch monitor <task_id> [--once]"
|
||||
exit 1
|
||||
}
|
||||
let once = ("--once" in $args) or ("-1" in $args)
|
||||
let result = (orch-monitor $task_id --once=$once)
|
||||
print $"Task: ($result.id)"
|
||||
print $" Status: ($result.status)"
|
||||
if $result.message? != null { print $" Message: ($result.message)" }
|
||||
}
|
||||
"help" | "--help" => { help-orch }
|
||||
_ => { print $"Unknown orchestrator command: [$action]"; help-orch; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
# Help for orchestrator commands
|
||||
def help-orch [] {
|
||||
print "Orchestrator - Local orchestrator operations"
|
||||
print ""
|
||||
print "Usage: provisioning orch <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " status Check orchestrator status"
|
||||
print " tasks List tasks in queue"
|
||||
print " validate <workflow.ncl> Validate Nickel workflow"
|
||||
print " submit <workflow.ncl> Submit workflow for execution"
|
||||
print " monitor <task_id> Monitor task progress"
|
||||
print ""
|
||||
print "Performance: 30x faster with nu_plugin_orchestrator vs HTTP"
|
||||
}
|
||||
80
nulib/cli/handlers/integrations/runtime.nu
Normal file
80
nulib/cli/handlers/integrations/runtime.nu
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Runtime Command Handler
|
||||
# Domain: Container runtime abstraction (docker, podman, orbstack, colima, nerdctl)
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
def runtime-detect [] { {name: "docker", command: "docker"} }
|
||||
def runtime-exec [command: string --check = false] { $"Executed: ($command)" }
|
||||
def runtime-compose [file: string] { $"Using compose file: ($file)" }
|
||||
def runtime-info [] { {name: "docker", available: true, version: "24.0.0"} }
|
||||
def runtime-list [] { [{name: "docker"} {name: "podman"}] }
|
||||
|
||||
export def cmd-runtime [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-runtime; return }
|
||||
|
||||
match $action {
|
||||
"detect" => {
|
||||
if $check {
|
||||
print "Would detect available container runtime"
|
||||
} else {
|
||||
let runtime = (runtime-detect)
|
||||
print $"Detected runtime: [$runtime.name]"
|
||||
print $"Command: [$runtime.command]"
|
||||
}
|
||||
}
|
||||
"exec" => {
|
||||
let command = ($args | get 0?)
|
||||
if ($command == null) {
|
||||
print "Error: Command required"
|
||||
print "Usage: provisioning runtime exec <command>"
|
||||
exit 1
|
||||
}
|
||||
let result = (runtime-exec $command --check=$check)
|
||||
print $result
|
||||
}
|
||||
"compose" => {
|
||||
let file = ($args | get 0?)
|
||||
if ($file == null) {
|
||||
print "Error: Compose file required"
|
||||
print "Usage: provisioning runtime compose <file>"
|
||||
exit 1
|
||||
}
|
||||
let cmd = (runtime-compose $file)
|
||||
print $cmd
|
||||
}
|
||||
"info" => {
|
||||
let info = (runtime-info)
|
||||
print $"Runtime: [$info.name]"
|
||||
print $"Available: [$info.available]"
|
||||
print $"Version: [$info.version]"
|
||||
}
|
||||
"list" => {
|
||||
let runtimes = (runtime-list)
|
||||
if ($runtimes | length) == 0 {
|
||||
print "No runtimes available"
|
||||
} else {
|
||||
print "Available runtimes:"
|
||||
$runtimes | each {|rt| print $" • ($rt.name)"}
|
||||
}
|
||||
}
|
||||
"help" | "--help" => { help-runtime }
|
||||
_ => { print $"Unknown runtime command: [$action]"; help-runtime; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
def help-runtime [] {
|
||||
print "Runtime abstraction - Unified interface for container runtimes"
|
||||
print ""
|
||||
print "Usage: provisioning runtime <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " detect Detect available runtime"
|
||||
print " exec <cmd> Execute command in runtime"
|
||||
print " compose <file> Adapt docker-compose file for detected runtime"
|
||||
print " info Show runtime information"
|
||||
print " list List all available runtimes"
|
||||
}
|
||||
101
nulib/cli/handlers/integrations/service.nu
Normal file
101
nulib/cli/handlers/integrations/service.nu
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Service Command Handler
|
||||
# Domain: Cross-platform service management (systemd, launchd, runit, openrc)
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
def service-install [name: string binary: string --check = false] { {name: $name} }
|
||||
def service-start [name: string --check = false] { {name: $name} }
|
||||
def service-stop [name: string --check = false] { {name: $name} }
|
||||
def service-restart [name: string --check = false] { {name: $name} }
|
||||
def service-status [name: string] { {name: $name, running: false} }
|
||||
def service-list [--filter: string = ""] { [] }
|
||||
def service-detect-init [] { "systemd" }
|
||||
|
||||
export def cmd-service [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-service; return }
|
||||
|
||||
match $action {
|
||||
"install" => {
|
||||
let name = ($args | get 0?)
|
||||
let binary = ($args | get 1?)
|
||||
if ($name == null or $binary == null) {
|
||||
print "Usage: provisioning service install <name> <binary> [options]"
|
||||
exit 1
|
||||
}
|
||||
let result = (service-install $name $binary --check=$check)
|
||||
print $"Service installed: [$result.name]"
|
||||
}
|
||||
"start" => {
|
||||
let name = ($args | get 0?)
|
||||
if ($name == null) {
|
||||
print "Usage: provisioning service start <name>"
|
||||
exit 1
|
||||
}
|
||||
let result = (service-start $name --check=$check)
|
||||
print $"Service started: [$result.name]"
|
||||
}
|
||||
"stop" => {
|
||||
let name = ($args | get 0?)
|
||||
if ($name == null) {
|
||||
print "Usage: provisioning service stop <name>"
|
||||
exit 1
|
||||
}
|
||||
let result = (service-stop $name --check=$check)
|
||||
print $"Service stopped: [$result.name]"
|
||||
}
|
||||
"restart" => {
|
||||
let name = ($args | get 0?)
|
||||
if ($name == null) {
|
||||
print "Usage: provisioning service restart <name>"
|
||||
exit 1
|
||||
}
|
||||
let result = (service-restart $name --check=$check)
|
||||
print $"Service restarted: [$result.name]"
|
||||
}
|
||||
"status" => {
|
||||
let name = ($args | get 0?)
|
||||
if ($name == null) {
|
||||
print "Usage: provisioning service status <name>"
|
||||
exit 1
|
||||
}
|
||||
let status = (service-status $name)
|
||||
print $"Service: [$status.name]"
|
||||
print $" Running: [$status.running]"
|
||||
}
|
||||
"list" => {
|
||||
let filter = ($args | get 0?)
|
||||
let services = (service-list --filter=$filter)
|
||||
if ($services | length) == 0 {
|
||||
print "No services found"
|
||||
} else {
|
||||
print "Services:"
|
||||
$services | each {|s| print $" • [$s.name] - Running: [$s.running]"}
|
||||
}
|
||||
}
|
||||
"detect-init" => {
|
||||
let init = (service-detect-init)
|
||||
print $"Detected init system: [$init]"
|
||||
}
|
||||
"help" | "--help" => { help-service }
|
||||
_ => { print $"Unknown service command: [$action]"; help-service; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
def help-service [] {
|
||||
print "Service management - Cross-platform service operations"
|
||||
print ""
|
||||
print "Usage: provisioning service <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " install <name> <binary> Install service"
|
||||
print " start <name> Start service"
|
||||
print " stop <name> Stop service"
|
||||
print " restart <name> Restart service"
|
||||
print " status <name> Check service status"
|
||||
print " list [filter] List services"
|
||||
print " detect-init Detect init system"
|
||||
}
|
||||
33
nulib/cli/handlers/integrations/shared.nu
Normal file
33
nulib/cli/handlers/integrations/shared.nu
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Shared Integration Utilities
|
||||
# Plugin detection, status checking, and flag parsing
|
||||
|
||||
# Check if a plugin is available
|
||||
export def is-plugin-available [plugin_name: string] {
|
||||
(plugin list | where name == $plugin_name | length) > 0
|
||||
}
|
||||
|
||||
# Check if provisioning plugins are loaded
|
||||
export def plugins-status [] {
|
||||
{
|
||||
auth: (is-plugin-available "nu_plugin_auth")
|
||||
kms: (is-plugin-available "nu_plugin_kms")
|
||||
orchestrator: (is-plugin-available "nu_plugin_orchestrator")
|
||||
}
|
||||
}
|
||||
|
||||
# Helper to parse flags from args
|
||||
export def parse-flag [args: list, long_flag: string, short_flag: string = ""] {
|
||||
let long_idx = ($args | enumerate | where item == $long_flag | get index | first | default null)
|
||||
if ($long_idx != null) {
|
||||
return ($args | get ($long_idx + 1) | default null)
|
||||
}
|
||||
|
||||
if ($short_flag | is-not-empty) {
|
||||
let short_idx = ($args | enumerate | where item == $short_flag | get index | first | default null)
|
||||
if ($short_idx != null) {
|
||||
return ($args | get ($short_idx + 1) | default null)
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
85
nulib/cli/handlers/integrations/ssh.nu
Normal file
85
nulib/cli/handlers/integrations/ssh.nu
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# SSH Command Handler
|
||||
# Domain: Advanced SSH operations with pooling and circuit breaker
|
||||
|
||||
use ./shared.nu *
|
||||
|
||||
def ssh-pool-connect [host: string user: string --check = false] { {host: $host, port: 22} }
|
||||
def ssh-pool-status [] { {connections: 0, capacity: 10} }
|
||||
def ssh-deployment-strategies [] { ["serial" "parallel" "batched"] }
|
||||
def ssh-retry-config [strategy: string max_retries: int] { {strategy: $strategy, max_retries: $max_retries} }
|
||||
def ssh-circuit-breaker-status [] { {state: "closed", failures: 0} }
|
||||
|
||||
export def cmd-ssh [
|
||||
action: string
|
||||
args: list = []
|
||||
--check = false
|
||||
] {
|
||||
if ($action == null) { help-ssh; return }
|
||||
|
||||
match $action {
|
||||
"pool" => {
|
||||
let subaction = ($args | get 0?)
|
||||
match $subaction {
|
||||
"connect" => {
|
||||
let host = ($args | get 1?)
|
||||
let user = ($args | get 2? | default "root")
|
||||
if ($host == null) {
|
||||
print "Usage: provisioning ssh pool connect <host> [user]"
|
||||
exit 1
|
||||
}
|
||||
let pool = (ssh-pool-connect $host $user --check=$check)
|
||||
print $"Connected to: [$pool.host]:[$pool.port]"
|
||||
}
|
||||
"exec" => { print "SSH pool execute: implementation pending" }
|
||||
"status" => {
|
||||
let status = (ssh-pool-status)
|
||||
print $"Pool status: [$status.connections] connections"
|
||||
}
|
||||
_ => { help-ssh-pool }
|
||||
}
|
||||
}
|
||||
"strategies" => {
|
||||
let strategies = (ssh-deployment-strategies)
|
||||
print "Deployment strategies:"
|
||||
$strategies | each {|s| print $" • $s"}
|
||||
}
|
||||
"retry-config" => {
|
||||
let strategy = ($args | get 0? | default "exponential")
|
||||
let max_retries = ($args | get 1? | default 3)
|
||||
let config = (ssh-retry-config $strategy $max_retries)
|
||||
print $"Retry config: [$config.strategy] with max [$config.max_retries] retries"
|
||||
}
|
||||
"circuit-breaker" => {
|
||||
let status = (ssh-circuit-breaker-status)
|
||||
print $"Circuit breaker state: [$status.state]"
|
||||
print $"Failures: [$status.failures]"
|
||||
}
|
||||
"help" | "--help" => { help-ssh }
|
||||
_ => { print $"Unknown ssh command: [$action]"; help-ssh; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
def help-ssh [] {
|
||||
print "SSH advanced - Distributed operations with pooling and circuit breaker"
|
||||
print ""
|
||||
print "Usage: provisioning ssh <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " pool connect <host> [user] Create SSH pool connection"
|
||||
print " pool exec <hosts> <cmd> Execute on SSH pool"
|
||||
print " pool status Check pool status"
|
||||
print " strategies List deployment strategies"
|
||||
print " retry-config [strategy] Configure retry strategy"
|
||||
print " circuit-breaker Check circuit breaker status"
|
||||
}
|
||||
|
||||
def help-ssh-pool [] {
|
||||
print "SSH pool operations"
|
||||
print ""
|
||||
print "Usage: provisioning ssh pool <action> [args]"
|
||||
print ""
|
||||
print "Actions:"
|
||||
print " connect <host> [user] Create connection"
|
||||
print " exec <hosts> <cmd> Execute command"
|
||||
print " status Check status"
|
||||
}
|
||||
151
nulib/cli/handlers/mod.nu
Normal file
151
nulib/cli/handlers/mod.nu
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# cli/handlers/ subsystem façade (ADR-026).
|
||||
export use cli/handlers/authentication.nu [
|
||||
handle_authentication_command
|
||||
]
|
||||
export use cli/handlers/build.nu [
|
||||
handle_build_command
|
||||
]
|
||||
export use cli/handlers/configuration.nu [
|
||||
handle_configuration_command
|
||||
]
|
||||
export use cli/handlers/development.nu [
|
||||
handle_development_command
|
||||
]
|
||||
export use cli/handlers/diagnostics.nu [
|
||||
handle_diagnostics_command
|
||||
]
|
||||
export use cli/handlers/generation.nu [
|
||||
handle_generation_command
|
||||
]
|
||||
export use cli/handlers/golden_image.nu [
|
||||
"image build"
|
||||
"image list"
|
||||
"image info"
|
||||
"image delete"
|
||||
"vm create-from-image"
|
||||
"image from-vm"
|
||||
"cache init"
|
||||
"cache list"
|
||||
"cache stats"
|
||||
"cache cleanup"
|
||||
"version create"
|
||||
"version list"
|
||||
"version rollback"
|
||||
"version deprecate"
|
||||
"golden-image-stats"
|
||||
]
|
||||
export use cli/handlers/guides.nu [
|
||||
handle_guide_command
|
||||
]
|
||||
export use cli/handlers/infrastructure.nu [
|
||||
handle_infrastructure_command
|
||||
handle_price_command
|
||||
handle_create_server_task
|
||||
]
|
||||
export use cli/handlers/nested_infrastructure.nu [
|
||||
"volume create"
|
||||
"volume list"
|
||||
"volume info"
|
||||
"volume attach"
|
||||
"volume snapshot"
|
||||
"network create"
|
||||
"network list"
|
||||
"network connect"
|
||||
"vlan create"
|
||||
"vlan list"
|
||||
"policy create"
|
||||
"nested-vm create"
|
||||
"nested-vm list"
|
||||
"container create"
|
||||
"container list"
|
||||
"deployment create"
|
||||
"deployment deploy"
|
||||
"deployment list"
|
||||
"deployment info"
|
||||
"deployment scale"
|
||||
"deployment health"
|
||||
"nested-infrastructure-stats"
|
||||
]
|
||||
export use cli/handlers/orchestration.nu [
|
||||
handle_orchestration_command
|
||||
]
|
||||
export use cli/handlers/platform.nu [
|
||||
handle_platform_command
|
||||
]
|
||||
export use cli/handlers/secretumvault.nu [
|
||||
handle_secretumvault_command
|
||||
]
|
||||
export use cli/handlers/setup_simple.nu [
|
||||
cmd-setup-simple
|
||||
]
|
||||
export use cli/handlers/setup.nu [
|
||||
cmd-setup
|
||||
]
|
||||
export use cli/handlers/state.nu [
|
||||
handle_state_command
|
||||
]
|
||||
export use cli/handlers/utilities_core.nu [
|
||||
handle_utility_command
|
||||
]
|
||||
export use cli/handlers/utilities_handlers.nu [
|
||||
handle_ssh
|
||||
handle_sops_edit
|
||||
handle_cache
|
||||
handle_providers
|
||||
handle_providers_list
|
||||
handle_providers_info
|
||||
handle_providers_install
|
||||
handle_providers_remove
|
||||
handle_providers_installed
|
||||
handle_providers_validate
|
||||
handle_nu
|
||||
handle_list
|
||||
handle_qr
|
||||
handle_nuinfo
|
||||
handle_plugins
|
||||
handle_plugin_list
|
||||
handle_plugin_register
|
||||
handle_plugin_test
|
||||
handle_plugin_build
|
||||
handle_plugin_status
|
||||
handle_guide
|
||||
]
|
||||
export use cli/handlers/vm_domain.nu [
|
||||
handle_vm_command
|
||||
]
|
||||
export use cli/handlers/vm_hosts.nu [
|
||||
"vm hosts check"
|
||||
"vm hosts prepare"
|
||||
"vm hosts list"
|
||||
"vm hosts status"
|
||||
"vm hosts ensure"
|
||||
]
|
||||
export use cli/handlers/vm_lifecycle.nu [
|
||||
"vm list-permanent"
|
||||
"vm list-temporary"
|
||||
"vm make-permanent"
|
||||
"vm make-temporary"
|
||||
"vm info-lifecycle"
|
||||
"vm cleanup-now"
|
||||
"vm extend-ttl"
|
||||
"vm scheduler start"
|
||||
"vm scheduler stop"
|
||||
"vm scheduler status"
|
||||
"vm cleanup-queue"
|
||||
"vm recovery-enable"
|
||||
"vm lifecycle-stats"
|
||||
]
|
||||
export use cli/handlers/runner_daemon.nu [
|
||||
handle_runner_command
|
||||
]
|
||||
export use cli/handlers/vm.nu [
|
||||
"vm create"
|
||||
"vm list"
|
||||
"vm start"
|
||||
"vm stop"
|
||||
"vm delete"
|
||||
"vm info"
|
||||
"vm ssh"
|
||||
"vm exec"
|
||||
"vm scp"
|
||||
]
|
||||
394
nulib/cli/handlers/nested_infrastructure.nu
Normal file
394
nulib/cli/handlers/nested_infrastructure.nu
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
# Nested Infrastructure CLI Commands (Phase 4)
|
||||
#
|
||||
# User-facing commands for nested VMs, volumes, networking, and deployments.
|
||||
|
||||
use platform/vm/ {
|
||||
"volume-create" "volume-list" "volume-info" "volume-attach" "volume-detach"
|
||||
"volume-snapshot" "volume-restore" "volume-delete" "volume-stats"
|
||||
"network-create" "network-list" "network-info" "network-connect" "network-disconnect"
|
||||
"network-policy-create" "network-policy-list" "network-stats" "vlan-create" "vlan-list"
|
||||
"nested-vm-create" "nested-vm-list" "nested-vm-info" "nested-vm-delete"
|
||||
"container-create" "container-list" "container-delete" "nesting-stats"
|
||||
"deployment-create" "deployment-deploy" "deployment-list" "deployment-info"
|
||||
"deployment-delete" "deployment-scale" "deployment-health"
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
# VOLUME MANAGEMENT COMMANDS
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export def "volume create" [
|
||||
name: string # Volume name
|
||||
size_gb: int # Size in GB
|
||||
--type: string = "local" # Type (local/nfs/cifs)
|
||||
--mount-path: string = "" # Mount path
|
||||
--readonly = false # Read-only
|
||||
]: table {
|
||||
"""
|
||||
Create a storage volume.
|
||||
|
||||
Examples:
|
||||
provisioning volume create data 100 --type local --mount-path /data
|
||||
provisioning volume create shared 500 --type nfs --mount-path /mnt/shared
|
||||
"""
|
||||
|
||||
let result = (volume-create $name $size_gb --type=$type --mount-path=$mount_path --readonly=$readonly)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Volume '($name)' created"
|
||||
print $" Size: ($result.size_gb)GB"
|
||||
print $" Path: ($result.volume_path)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "volume list" []: table {
|
||||
"""List all volumes."""
|
||||
volume-list
|
||||
}
|
||||
|
||||
export def "volume info" [name: string]: record {
|
||||
"""Get volume information."""
|
||||
volume-info $name
|
||||
}
|
||||
|
||||
export def "volume attach" [
|
||||
volume_name: string
|
||||
vm_name: string
|
||||
--mount-path: string = ""
|
||||
]: table {
|
||||
"""Attach volume to VM."""
|
||||
let result = (volume-attach $volume_name $vm_name --mount-path=$mount_path)
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "volume snapshot" [
|
||||
volume_name: string
|
||||
snapshot_name: string
|
||||
--description: string = ""
|
||||
]: table {
|
||||
"""Create volume snapshot."""
|
||||
let result = (volume-snapshot $volume_name $snapshot_name --description=$description)
|
||||
[$result]
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
# NETWORKING COMMANDS
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export def "network create" [
|
||||
name: string # Network name
|
||||
subnet: string # Subnet CIDR
|
||||
--type: string = "bridge" # Type
|
||||
--gateway: string = "" # Gateway
|
||||
]: table {
|
||||
"""
|
||||
Create a virtual network.
|
||||
|
||||
Examples:
|
||||
provisioning network create frontend 192.168.1.0/24
|
||||
provisioning network create backend 192.168.2.0/24 --type vlan
|
||||
"""
|
||||
|
||||
let result = (network-create $name $subnet --type=$type --gateway=$gateway)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Network '($name)' created"
|
||||
print $" Subnet: ($result.subnet)"
|
||||
print $" Gateway: ($result.gateway)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "network list" []: table {
|
||||
"""List all networks."""
|
||||
network-list
|
||||
}
|
||||
|
||||
export def "network connect" [
|
||||
network_name: string
|
||||
vm_name: string
|
||||
--static-ip: string = ""
|
||||
]: table {
|
||||
"""Connect VM to network."""
|
||||
let result = (network-connect $network_name $vm_name --static-ip=$static_ip)
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "vlan create" [
|
||||
vlan_id: int
|
||||
name: string
|
||||
subnet: string
|
||||
]: table {
|
||||
"""Create a VLAN."""
|
||||
let result = (vlan-create $vlan_id $name $subnet)
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "vlan list" []: table {
|
||||
"""List all VLANs."""
|
||||
vlan-list
|
||||
}
|
||||
|
||||
export def "policy create" [
|
||||
name: string
|
||||
--direction: string = "both"
|
||||
--protocol: string = "tcp"
|
||||
--source: string = "any"
|
||||
--destination: string = "any"
|
||||
--action: string = "allow"
|
||||
]: table {
|
||||
"""Create network policy."""
|
||||
let result = (network-policy-create $name --direction=$direction --protocol=$protocol --source=$source --destination=$destination --action=$action)
|
||||
[$result]
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
# NESTED VM COMMANDS
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export def "nested-vm create" [
|
||||
name: string
|
||||
parent_vm: string
|
||||
--cpu: int = 2
|
||||
--memory: int = 2048
|
||||
--disk: int = 20
|
||||
--networks: list<string> = []
|
||||
--volumes: list<string> = []
|
||||
--auto-start = false
|
||||
]: table {
|
||||
"""
|
||||
Create a nested VM inside another VM.
|
||||
|
||||
Examples:
|
||||
provisioning nested-vm create app-01 web-server --cpu 4 --memory 4096
|
||||
provisioning nested-vm create db-01 web-server --networks backend --volumes data
|
||||
"""
|
||||
|
||||
let result = (
|
||||
nested-vm-create $name $parent_vm \
|
||||
--cpu=$cpu --memory=$memory --disk=$disk \
|
||||
--networks=$networks --volumes=$volumes --auto-start=$auto_start
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Nested VM '($name)' created"
|
||||
print $" Parent: ($result.parent_vm)"
|
||||
print $" CPU: ($result.cpu) cores"
|
||||
print $" Memory: ($result.memory_mb)MB"
|
||||
print $" Nesting Depth: ($result.nesting_depth)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "nested-vm list" [--parent: string = ""]: table {
|
||||
"""List nested VMs."""
|
||||
nested-vm-list $parent
|
||||
}
|
||||
|
||||
export def "container create" [
|
||||
name: string
|
||||
image: string
|
||||
parent_vm: string
|
||||
--tag: string = "latest"
|
||||
--cpu-millicores: int = 1000
|
||||
--memory-mb: int = 512
|
||||
--networks: list<string> = []
|
||||
--volumes: list<string> = []
|
||||
]: table {
|
||||
"""
|
||||
Create a container inside a nested VM.
|
||||
|
||||
Examples:
|
||||
provisioning container create web-app nginx app-01
|
||||
provisioning container create api-server myapp:v1.0 app-01 --cpu-millicores 2000 --memory-mb 1024
|
||||
"""
|
||||
|
||||
let result = (
|
||||
container-create $name $image $parent_vm \
|
||||
--tag=$tag --cpu-millicores=$cpu_millicores --memory-mb=$memory_mb \
|
||||
--networks=$networks --volumes=$volumes
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Container '($name)' created"
|
||||
print $" Image: ($result.image)"
|
||||
print $" Parent VM: ($result.parent_vm)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "container list" [--parent: string = ""]: table {
|
||||
"""List containers."""
|
||||
container-list $parent
|
||||
}
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
# MULTI-TIER DEPLOYMENT COMMANDS
|
||||
# ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
export def "deployment create" [
|
||||
name: string
|
||||
--tiers: list<string>
|
||||
--replicas: int = 1
|
||||
--version: string = "1.0.0"
|
||||
]: table {
|
||||
"""
|
||||
Create a multi-tier application deployment.
|
||||
|
||||
Examples:
|
||||
provisioning deployment create myapp --tiers web app db --replicas 3
|
||||
provisioning deployment create ecommerce --tiers frontend backend database --replicas 2 --version 1.2.0
|
||||
"""
|
||||
|
||||
let result = (
|
||||
deployment-create $name \
|
||||
--tiers=$tiers --replicas=$replicas --version=$version
|
||||
)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Deployment '($name)' created"
|
||||
print $" Version: ($result.version)"
|
||||
print $" Tiers: ($result.tiers | str join ', ')"
|
||||
print $" Replicas: ($result.replicas)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "deployment deploy" [
|
||||
name: string
|
||||
--check = false
|
||||
]: table {
|
||||
"""
|
||||
Deploy a multi-tier application.
|
||||
|
||||
Examples:
|
||||
provisioning deployment deploy myapp
|
||||
provisioning deployment deploy myapp --check # Dry-run
|
||||
"""
|
||||
|
||||
let result = (deployment-deploy $name --check=$check)
|
||||
|
||||
if $check {
|
||||
print "Deployment configuration:"
|
||||
print $result.would_deploy | to json
|
||||
return [$result]
|
||||
}
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Deployment '($name)' deployed"
|
||||
print $" Instances: ($result.instances_deployed)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "deployment list" []: table {
|
||||
"""List deployments."""
|
||||
deployment-list
|
||||
}
|
||||
|
||||
export def "deployment info" [name: string]: record {
|
||||
"""Get deployment information."""
|
||||
deployment-info $name
|
||||
}
|
||||
|
||||
export def "deployment scale" [
|
||||
name: string
|
||||
tier: string
|
||||
replicas: int
|
||||
]: table {
|
||||
"""
|
||||
Scale a deployment tier.
|
||||
|
||||
Examples:
|
||||
provisioning deployment scale myapp web 5 # Scale web tier to 5 replicas
|
||||
provisioning deployment scale myapp db 2 # Scale db tier to 2 replicas
|
||||
"""
|
||||
|
||||
let result = (deployment-scale $name $tier $replicas)
|
||||
|
||||
if $result.success {
|
||||
print $"✓ Scaled '($tier)' to ($replicas) replicas"
|
||||
print $" Previous: ($result.previous_replicas)"
|
||||
print $" New: ($result.new_replicas)"
|
||||
} else {
|
||||
print $"✗ Failed: ($result.error)"
|
||||
}
|
||||
|
||||
[$result]
|
||||
}
|
||||
|
||||
export def "deployment health" [name: string]: record {
|
||||
"""Check deployment health."""
|
||||
let health = (deployment-health $name)
|
||||
|
||||
if $health.success {
|
||||
print $"Deployment Health: '($name)'"
|
||||
print $" Instances: ($health.total_instances)"
|
||||
print $" Healthy: ($health.healthy)"
|
||||
print $" Unhealthy: ($health.unhealthy)"
|
||||
print $" Health %: ($health.health_percent)%"
|
||||
}
|
||||
|
||||
$health
|
||||
}
|
||||
|
||||
export def "nested-infrastructure-stats" []: record {
|
||||
"""
|
||||
Show nested infrastructure statistics.
|
||||
|
||||
Comprehensive view of all nested components.
|
||||
"""
|
||||
|
||||
let nesting = (nesting-stats)
|
||||
let volumes = (volume-stats)
|
||||
let networks = (network-stats)
|
||||
let deployments = (deployment-list)
|
||||
|
||||
print "Nested Infrastructure Statistics"
|
||||
print "════════════════════════════════════════"
|
||||
print ""
|
||||
print "Nesting:"
|
||||
print $" Nested VMs: ($nesting.nested_vms)"
|
||||
print $" Containers: ($nesting.containers)"
|
||||
print $" Max Depth: ($nesting.max_nesting_depth)"
|
||||
print ""
|
||||
print "Storage:"
|
||||
print $" Total Volumes: ($volumes.total_volumes)"
|
||||
print $" Total Size: ($volumes.total_size_gb)GB"
|
||||
print $" Utilization: ($volumes.utilization_percent)%"
|
||||
print ""
|
||||
print "Networking:"
|
||||
print $" Networks: ($networks.total_networks)"
|
||||
print $" Policies: ($networks.total_policies)"
|
||||
print $" Connections: ($networks.total_connections)"
|
||||
print ""
|
||||
print "Deployments:"
|
||||
print $" Total: ($deployments | length)"
|
||||
print ""
|
||||
|
||||
{
|
||||
nesting: $nesting
|
||||
volumes: $volumes
|
||||
networks: $networks
|
||||
deployments: ($deployments | length)
|
||||
}
|
||||
}
|
||||
295
nulib/cli/handlers/orchestration.nu
Normal file
295
nulib/cli/handlers/orchestration.nu
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
# Orchestration Command Handlers
|
||||
# Handles: job (orchestrator jobs), workflow (WorkflowDef), batch, orchestrator commands
|
||||
|
||||
use cli/flags.nu *
|
||||
# REMOVED: use ../../lib_provisioning * - causes circular import
|
||||
use platform/plugins/auth.nu *
|
||||
use platform/mod.nu *
|
||||
|
||||
# Helper to run module commands
|
||||
def run_module [
|
||||
args: string
|
||||
module: string
|
||||
option?: string
|
||||
--exec
|
||||
] {
|
||||
let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" }
|
||||
|
||||
if $exec {
|
||||
exec $"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
} else {
|
||||
^$"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args
|
||||
}
|
||||
}
|
||||
|
||||
# Main orchestration command dispatcher
|
||||
export def handle_orchestration_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
set_debug_env $flags
|
||||
|
||||
match $command {
|
||||
"job" => { handle_job $ops $flags }
|
||||
"workflow" => { handle_workflowdef $ops $flags }
|
||||
"batch" => { handle_batch $ops $flags }
|
||||
"orchestrator" => { handle_orchestrator $ops $flags }
|
||||
_ => {
|
||||
print $"❌ Unknown orchestration command: ($command)"
|
||||
print ""
|
||||
print "Available orchestration commands:"
|
||||
print " job - Orchestrator job management (list, status, monitor, submit)"
|
||||
print " workflow - Workspace WorkflowDef management (list, show, run, validate, status)"
|
||||
print " batch - Batch operations (submit, monitor, rollback, stats)"
|
||||
print " orchestrator - Orchestrator lifecycle (start, stop, status, health)"
|
||||
print ""
|
||||
print "Use 'provisioning help orchestration' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Job command handler — orchestrator HTTP API jobs
|
||||
def handle_job [ops: string, flags: record] {
|
||||
# Authentication check for workflow operations (metadata-driven)
|
||||
let operation_parts = ($ops | split row " ")
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Determine operation type
|
||||
let operation_type = match $action {
|
||||
"rollback" | "cancel" => "delete"
|
||||
"submit" | "create" => "create"
|
||||
"update" | "modify" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
# Check authentication using metadata-driven approach
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"job ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
# Call job management commands directly (avoid -mod routing conflict)
|
||||
use orchestration/workflows/management.nu *
|
||||
|
||||
let orchestrator = ($flags.orchestrator? | default "")
|
||||
let status_filter = ($flags.status? | default "")
|
||||
let days = ($flags.days? | default 7 | into int)
|
||||
let dry_run = ($flags.dry_run? | default false)
|
||||
|
||||
# DEBUG
|
||||
if $action == "browse" {
|
||||
print $"DEBUG: Handling browse action, ops=($ops)"
|
||||
}
|
||||
|
||||
match $action {
|
||||
"list" => {
|
||||
let limit_arg = if ($operation_parts | length) > 1 {
|
||||
let limit_str = ($operation_parts | get 1 | str trim)
|
||||
let result = (do { $limit_str | into int } | complete)
|
||||
if $result.exit_code == 0 { ($result.stdout | str trim | into int) } else { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if ($limit_arg | is-not-empty) {
|
||||
job list $limit_arg --orchestrator $orchestrator --status $status_filter
|
||||
} else {
|
||||
job list --orchestrator $orchestrator --status $status_filter
|
||||
}
|
||||
}
|
||||
"status" => {
|
||||
if ($operation_parts | length) < 2 {
|
||||
print "❌ Error: job status requires a task ID"
|
||||
return
|
||||
}
|
||||
let task_id = ($operation_parts | get 1)
|
||||
job status $task_id --orchestrator $orchestrator
|
||||
}
|
||||
"monitor" => {
|
||||
if ($operation_parts | length) < 2 {
|
||||
print "❌ Error: job monitor requires a task ID"
|
||||
return
|
||||
}
|
||||
let task_id = ($operation_parts | get 1)
|
||||
job monitor $task_id --orchestrator $orchestrator
|
||||
}
|
||||
"stats" => { job stats --orchestrator $orchestrator }
|
||||
"cleanup" => {
|
||||
if $dry_run {
|
||||
job cleanup --orchestrator $orchestrator --days $days --dry-run
|
||||
} else {
|
||||
job cleanup --orchestrator $orchestrator --days $days
|
||||
}
|
||||
}
|
||||
"orchestrator" => { job orchestrator --orchestrator $orchestrator }
|
||||
"browse" => {
|
||||
let limit_arg = if ($operation_parts | length) > 1 {
|
||||
let limit_str = ($operation_parts | get 1 | str trim)
|
||||
let result = (do { $limit_str | into int } | complete)
|
||||
if $result.exit_code == 0 { ($result.stdout | str trim | into int) } else { null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if ($limit_arg | is-not-empty) {
|
||||
job browse $limit_arg --orchestrator $orchestrator
|
||||
} else {
|
||||
job browse --orchestrator $orchestrator
|
||||
}
|
||||
}
|
||||
"submit" => {
|
||||
if ($operation_parts | length) < 4 {
|
||||
print "❌ Error: job submit requires: job_type operation target [infra] [settings]"
|
||||
return
|
||||
}
|
||||
let job_type = ($operation_parts | get 1)
|
||||
let operation_name = ($operation_parts | get 2)
|
||||
let target = ($operation_parts | get 3)
|
||||
let infra = if ($operation_parts | length) > 4 { $operation_parts | get 4 } else { "" }
|
||||
let settings = if ($operation_parts | length) > 5 { $operation_parts | get 5 } else { "" }
|
||||
let check_mode = (is-check-mode $flags)
|
||||
let wait = ($flags.wait? | default false)
|
||||
|
||||
job submit $job_type $operation_name $target $infra $settings --check=$check_mode --wait=$wait --orchestrator $orchestrator
|
||||
}
|
||||
"" => {
|
||||
print "❌ Error: job subcommand required — use: list, status, monitor, stats, cleanup, browse, submit"
|
||||
return
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Error: unknown job subcommand '$action'"
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# WorkflowDef command handler — workspace workflow declarations (workflows/*.ncl)
|
||||
def handle_workflowdef [ops: string, flags: record] {
|
||||
let parts = ($ops | split row " ")
|
||||
let action = if ($parts | is-empty) { "" } else { $parts | first }
|
||||
let workspace = ($flags.workspace? | default ($flags.ws? | default ""))
|
||||
let infra = ($flags.infra? | default "")
|
||||
let dry_run = ($flags.dry_run? | default false)
|
||||
|
||||
use ../../cli/workflow.nu *
|
||||
|
||||
match $action {
|
||||
"list" => {
|
||||
if ($workspace | is-not-empty) {
|
||||
main workflow list --workspace $workspace
|
||||
} else {
|
||||
main workflow list
|
||||
}
|
||||
}
|
||||
"show" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: workflow show requires a workflow id"
|
||||
return
|
||||
}
|
||||
let wf_id = ($parts | get 1)
|
||||
if ($workspace | is-not-empty) {
|
||||
use ../../cli/ontoref_queries.nu *
|
||||
main describe workflow $wf_id --workspace $workspace
|
||||
} else {
|
||||
use ../../cli/ontoref_queries.nu *
|
||||
main describe workflow $wf_id
|
||||
}
|
||||
}
|
||||
"run" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: workflow run requires a workflow id"
|
||||
return
|
||||
}
|
||||
let wf_id = ($parts | get 1)
|
||||
if ($workspace | is-not-empty) and $dry_run {
|
||||
main workflow run $wf_id --workspace $workspace --dry-run
|
||||
} else if ($workspace | is-not-empty) {
|
||||
main workflow run $wf_id --workspace $workspace
|
||||
} else if $dry_run {
|
||||
main workflow run $wf_id --dry-run
|
||||
} else {
|
||||
main workflow run $wf_id
|
||||
}
|
||||
}
|
||||
"validate" => {
|
||||
if ($workspace | is-not-empty) {
|
||||
main workflow validate --workspace $workspace
|
||||
} else {
|
||||
main workflow validate
|
||||
}
|
||||
}
|
||||
"status" => {
|
||||
if ($parts | length) < 2 {
|
||||
print "❌ Error: workflow status requires a workflow id"
|
||||
return
|
||||
}
|
||||
let wf_id = ($parts | get 1)
|
||||
if ($workspace | is-not-empty) {
|
||||
main workflow status $wf_id --workspace $workspace
|
||||
} else {
|
||||
main workflow status $wf_id
|
||||
}
|
||||
}
|
||||
"" => {
|
||||
print "❌ Error: workflow subcommand required"
|
||||
print ""
|
||||
print " list [--workspace <ws>] List workspace WorkflowDef declarations"
|
||||
print " show <id> [--workspace <ws>] Show workflow definition + FSM state"
|
||||
print " run <id> [--workspace <ws>] Execute a WorkflowDef"
|
||||
print " validate [--workspace <ws>] Cross-validate workflows against components"
|
||||
print " status <id> [--workspace <ws>] Show FSM dimension state"
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown workflow subcommand: ($action)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Batch command handler
|
||||
def handle_batch [ops: string, flags: record] {
|
||||
# Authentication check for batch operations (metadata-driven)
|
||||
let operation_parts = ($ops | split row " ")
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Determine operation type
|
||||
let operation_type = match $action {
|
||||
"submit" | "create" => "create"
|
||||
"rollback" | "cancel" => "delete"
|
||||
"update" | "modify" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
# Check authentication using metadata-driven approach
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"batch ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
let args = build_module_args $flags $ops
|
||||
run_module $args "batch" --exec
|
||||
}
|
||||
|
||||
# Orchestrator command handler
|
||||
def handle_orchestrator [ops: string, flags: record] {
|
||||
# Authentication check for orchestrator operations (metadata-driven)
|
||||
let operation_parts = ($ops | split row " ")
|
||||
let action = if ($operation_parts | is-empty) { "" } else { $operation_parts | first }
|
||||
|
||||
# Determine operation type (orchestrator start/stop are modify operations)
|
||||
let operation_type = match $action {
|
||||
"start" => "modify"
|
||||
"stop" => "modify"
|
||||
_ => "read"
|
||||
}
|
||||
|
||||
# Check authentication using metadata-driven approach
|
||||
if not (is-check-mode $flags) and $operation_type != "read" {
|
||||
let operation_name = $"orchestrator ($action)"
|
||||
check-operation-auth $operation_name $operation_type $flags
|
||||
}
|
||||
|
||||
# Orchestrator has simpler argument requirements
|
||||
run_module $ops "orchestrator" --exec
|
||||
}
|
||||
890
nulib/cli/handlers/platform.nu
Normal file
890
nulib/cli/handlers/platform.nu
Normal file
|
|
@ -0,0 +1,890 @@
|
|||
# Platform Command Handlers
|
||||
# Handles: platform status, config, health, start, connections, etc.
|
||||
|
||||
use cli/flags.nu *
|
||||
use platform/mod.nu *
|
||||
use platform/service_manager.nu [ncl-sync-start ncl-sync-stop ncl-sync-status]
|
||||
use platform/user/config.nu [get-platform-config-dir write-provisioning-root]
|
||||
|
||||
# Main platform command dispatcher
|
||||
export def handle_platform_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
# Parse subcommand from ops if present
|
||||
let parts = ($ops | split row " " | where { |x| ($x | is-not-empty) })
|
||||
let actual_command = if ($parts | length) > 0 { ($parts | get 0) } else { "" }
|
||||
let remaining_args = if ($parts | length) > 1 { ($parts | skip 1) } else { [] }
|
||||
|
||||
match $actual_command {
|
||||
"start" => { platform-start $remaining_args $flags }
|
||||
"stop" => { platform-stop $remaining_args }
|
||||
"restart" => { platform-restart $remaining_args $flags }
|
||||
"status" | "st" => { platform-status $remaining_args }
|
||||
"external" | "ext" => { platform-external }
|
||||
"health" => { platform-health }
|
||||
"check" => { platform-check }
|
||||
"list" => { platform-list }
|
||||
"config" => { platform-config }
|
||||
"connections" => { platform-connections }
|
||||
"init" => { platform-init }
|
||||
"logs" | "log" => { platform-logs $remaining_args }
|
||||
"help" | "" => { show-platform-help }
|
||||
_ => {
|
||||
print $"❌ Unknown platform command: ($actual_command)"
|
||||
print ""
|
||||
show-platform-help
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Platform Command Implementations
|
||||
# ============================================================================
|
||||
|
||||
def platform-start [args: list<string>, flags: record] {
|
||||
# Persist provisioning root so Rust binaries can resolve schema imports
|
||||
# without requiring $PROVISIONING in their environment.
|
||||
write-provisioning-root
|
||||
|
||||
# Known deployment modes
|
||||
let known_modes = ["local" "docker" "kubernetes"]
|
||||
|
||||
# Determine if first arg is a mode or service name
|
||||
let is_mode_spec = (
|
||||
if ($args | length) > 0 {
|
||||
let first_arg = $args | get 0
|
||||
$known_modes | any { |m| $m == $first_arg }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
)
|
||||
|
||||
# If first arg is NOT a mode, treat all args as service names
|
||||
if (not $is_mode_spec) and ($args | length) > 0 {
|
||||
# ncl-sync doesn't follow the provisioning-* binary convention — handle separately.
|
||||
let has_ncl_sync = ($args | any {|s| $s == "ncl-sync" or $s == "ncl_sync"})
|
||||
if $has_ncl_sync {
|
||||
ncl-sync-start
|
||||
let rest = ($args | where {|s| $s != "ncl-sync" and $s != "ncl_sync"})
|
||||
if ($rest | is-not-empty) { start-services $rest }
|
||||
} else {
|
||||
start-services $args
|
||||
}
|
||||
print ""
|
||||
platform-status
|
||||
return
|
||||
}
|
||||
|
||||
# Otherwise, determine mode: from argument or from deployment-mode.ncl
|
||||
let mode = (
|
||||
if $is_mode_spec {
|
||||
$args | get 0
|
||||
} else {
|
||||
# Read mode from deployment-mode.ncl
|
||||
let deployment = (load-deployment-mode)
|
||||
$deployment.mode
|
||||
}
|
||||
)
|
||||
match $mode {
|
||||
"local" => {
|
||||
# Use configuration from deployment-mode.ncl to determine which services to start
|
||||
start-required-services
|
||||
|
||||
# Show status table after start
|
||||
sleep 3sec
|
||||
print ""
|
||||
platform-status
|
||||
return
|
||||
# Define service registry with correct binary names and startup args
|
||||
# Note: Some services have known issues and are marked as experimental
|
||||
let services_registry = {
|
||||
"vault-service": {port: 8081, binary: "provisioning-vault-service", protocol: "gRPC", args: "--port", status: "stable"}
|
||||
"catalog-registry": {port: 8082, binary: "provisioning-catalog-registry", protocol: "HTTP", args: "--port --host 127.0.0.1", status: "experimental"}
|
||||
"control-center": {port: 8000, binary: "provisioning-control-center", protocol: "HTTP/WebSocket", args: "--port", status: "stable"}
|
||||
"provisioning-rag": {port: 8300, binary: "provisioning-rag", protocol: "REST", args: "--mode solo", status: "stable"}
|
||||
"ai-service": {port: 8083, binary: "provisioning-ai-service", protocol: "HTTP", args: "--port --host 127.0.0.1 --mode solo", status: "stable"}
|
||||
"mcp-server": {port: 8400, binary: "provisioning-mcp-server", protocol: "Binary", args: "", status: "experimental"}
|
||||
"provisioning-nu-daemon": {port: 8100, binary: "provisioning-nu-daemon", protocol: "gRPC", args: "", status: "stable"}
|
||||
"orchestrator": {port: 9090, binary: "provisioning-orchestrator", protocol: "HTTP", args: "--port", status: "stable"}
|
||||
"detector": {port: 8600, binary: "provisioning-detector", protocol: "HTTP", args: "", status: "experimental"}
|
||||
"control-center-ui": {port: 3000, binary: "provisioning-control-center-ui", protocol: "HTTP (WASM)", args: "", status: "missing"}
|
||||
}
|
||||
|
||||
let service_groups = {
|
||||
"core": ["orchestrator"]
|
||||
"essential": ["vault-service", "provisioning-nu-daemon", "orchestrator"]
|
||||
"stable": ["vault-service", "provisioning-rag", "ai-service", "provisioning-nu-daemon", "orchestrator", "control-center"]
|
||||
"experimental": ["catalog-registry", "mcp-server", "detector"]
|
||||
"all": ["vault-service", "provisioning-rag", "ai-service", "catalog-registry", "mcp-server", "provisioning-nu-daemon", "orchestrator", "control-center", "detector"]
|
||||
}
|
||||
|
||||
# Determine which services to start
|
||||
let services_set = ($flags | get --optional services | default "core")
|
||||
let services_to_start = (
|
||||
if $services_set == "all" {
|
||||
$service_groups.all
|
||||
} else if $services_set == "essential" {
|
||||
$service_groups.essential
|
||||
} else if $services_set == "stable" {
|
||||
$service_groups.stable
|
||||
} else if $services_set == "experimental" {
|
||||
$service_groups.experimental
|
||||
} else if $services_set == "custom" {
|
||||
# TODO: Handle custom services from args
|
||||
$service_groups.core
|
||||
} else {
|
||||
$service_groups.core
|
||||
}
|
||||
)
|
||||
|
||||
# Display startup information
|
||||
print ""
|
||||
print "🚀 Starting Platform Services (Local Binary Mode)"
|
||||
print "═══════════════════════════════════════════════════"
|
||||
print ""
|
||||
print $"📋 Service Set: ($services_set)"
|
||||
print $"🔄 Services to start: ($services_to_start | length)"
|
||||
print ""
|
||||
print "Service Sets Available:"
|
||||
print " • core (default): Minimal working setup - orchestrator only"
|
||||
print " • essential: Recommended minimum - vault-service, daemon, orchestrator"
|
||||
print " • stable: All production-ready services"
|
||||
print " • experimental: Experimental/testing services"
|
||||
print " • all: All platform services (including experimental)"
|
||||
print ""
|
||||
|
||||
# Create logs directory
|
||||
let log_dir = $"($env.HOME? | default "~")/.provisioning/logs"
|
||||
(do { mkdir ($log_dir | path expand) } | ignore)
|
||||
|
||||
# Start each service
|
||||
let started_count = ($services_to_start | length)
|
||||
let max_index = (($services_to_start | length) - 1)
|
||||
let started_indices = (0..$max_index)
|
||||
|
||||
for i in $started_indices {
|
||||
let service_name = $services_to_start | get $i
|
||||
let service_info = $services_registry | get $service_name
|
||||
let port = $service_info.port
|
||||
let binary = $service_info.binary
|
||||
let protocol = $service_info.protocol
|
||||
let args_template = $service_info.args
|
||||
let log_file = $"($log_dir | path expand)/($service_name).log"
|
||||
|
||||
let index_num = ($i + 1)
|
||||
print $" [($index_num)] Starting ($service_name) on port ($port) — ($protocol)"
|
||||
|
||||
# Initialize vault service if needed
|
||||
if $service_name == "vault-service" {
|
||||
let local_bin_dir = ($env.HOME? | default "~" | path expand | path join ".local/bin")
|
||||
let init_script = ($local_bin_dir | path join "provisioning-init-vault")
|
||||
if ($init_script | path exists) {
|
||||
(^bash $init_script out+err> /dev/null | ignore)
|
||||
} else {
|
||||
print $" ⚠️ Vault initialization script not found"
|
||||
}
|
||||
}
|
||||
|
||||
# Check if binary exists in $HOME/.local/bin
|
||||
let local_bin_dir = ($env.HOME? | default "~" | path expand | path join ".local/bin")
|
||||
let binary_path = ($local_bin_dir | path join $binary)
|
||||
|
||||
if not ($binary_path | path exists) {
|
||||
print $" ✗ Binary not found: ($binary_path)"
|
||||
print $" ℹ Install with: just install"
|
||||
} else {
|
||||
# Build command with appropriate arguments
|
||||
let cmd_args = (
|
||||
if ($args_template | str contains "--port") {
|
||||
# Replace --port placeholder with actual port
|
||||
$args_template | str replace "--port" $"--port ($port)"
|
||||
} else if ($args_template | is-not-empty) {
|
||||
$args_template
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)
|
||||
|
||||
# Add config if available
|
||||
let config_args = ($service_info | get --optional config | default "")
|
||||
let full_args = if ($config_args | is-not-empty) {
|
||||
$"($cmd_args) ($config_args)"
|
||||
} else {
|
||||
$cmd_args
|
||||
} | str trim
|
||||
|
||||
# Start the service in background using nohup
|
||||
# This properly detaches the process from the terminal
|
||||
# Set up environment variables for specific services
|
||||
let home_expanded = ($env.HOME? | default "~" | path expand)
|
||||
let env_vars = if $service_name == "vault-service" {
|
||||
# Use development mode with Age KMS (can switch to secretumvault in production)
|
||||
$"PROVISIONING_ENV=dev AGE_PUBLIC_KEY_PATH=($home_expanded)/.config/provisioning/age/public_key.txt AGE_PRIVATE_KEY_PATH=($home_expanded)/.config/provisioning/age/private_key.txt"
|
||||
} else if $service_name == "control-center" {
|
||||
$"PROVISIONING_USER_PLATFORM=($home_expanded)/.config/provisioning/platform"
|
||||
} else if $service_name == "mcp-server" {
|
||||
# MCP server needs provisioning path set
|
||||
$"PROVISIONING_PATH=($home_expanded)/Development/provisioning"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
let start_cmd = if ($env_vars | is-not-empty) {
|
||||
$"nohup env ($env_vars) ($binary_path) ($full_args) >>($log_file) 2>&1 &"
|
||||
} else {
|
||||
$"nohup ($binary_path) ($full_args) >>($log_file) 2>&1 &"
|
||||
}
|
||||
(^sh -c $start_cmd | ignore)
|
||||
sleep 800ms
|
||||
let log_msg = $"logs: ($log_file)"
|
||||
print $" ✓ Started on port ($port) ($log_msg)"
|
||||
print $" Process may be initializing..."
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
print "Service Status:"
|
||||
print $" • Requested to start: ($started_count)"
|
||||
print $" • Logs directory: ($log_dir)"
|
||||
print ""
|
||||
|
||||
# Show status table after start
|
||||
sleep 1sec
|
||||
print ""
|
||||
platform-status
|
||||
|
||||
print "Next steps:"
|
||||
print " tail -f $($log_dir)/*.log # Monitor logs in real-time"
|
||||
print " provisioning platform health # Health checks"
|
||||
print " provisioning platform stop # Stop services"
|
||||
print ""
|
||||
print "Note: Services may take time to initialize. Check logs for startup details."
|
||||
}
|
||||
"docker" => {
|
||||
print "🐳 Docker Compose Mode"
|
||||
print " • Start services via docker-compose"
|
||||
print " • Uses docker-compose.yml from deployment config"
|
||||
print ""
|
||||
|
||||
let provisioning_path = ($env.PROVISIONING? | default "/usr/local/provisioning")
|
||||
let docker_compose_file = $"($provisioning_path)/platform/docker-compose.yml"
|
||||
|
||||
if ($docker_compose_file | path exists) {
|
||||
print "Starting with docker-compose..."
|
||||
(^docker-compose -f $docker_compose_file up -d out+err> /dev/null | ignore)
|
||||
print "✓ Services started"
|
||||
} else {
|
||||
print $"⚠ docker-compose.yml not found at ($docker_compose_file)"
|
||||
print " Create it with: provisioning generate docker-compose"
|
||||
}
|
||||
}
|
||||
"kubernetes" => {
|
||||
print "☸️ Kubernetes Mode"
|
||||
print " • Deploy to Kubernetes cluster"
|
||||
print " • Uses kubectl and manifests"
|
||||
print ""
|
||||
print "TODO: Implement Kubernetes deployment"
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown mode: ($mode)"
|
||||
print ""
|
||||
print "Available modes: local, docker, kubernetes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def platform-stop [args: list<string>] {
|
||||
print ""
|
||||
|
||||
# If service names are provided, stop only those services
|
||||
if ($args | length) > 0 {
|
||||
# ncl-sync doesn't follow the provisioning-* binary convention — handle separately.
|
||||
let has_ncl_sync = ($args | any {|s| $s == "ncl-sync" or $s == "ncl_sync"})
|
||||
if $has_ncl_sync {
|
||||
ncl-sync-stop
|
||||
print "✓ ncl-sync stopped"
|
||||
let rest = ($args | where {|s| $s != "ncl-sync" and $s != "ncl_sync"})
|
||||
if ($rest | is-not-empty) { stop-services $rest }
|
||||
} else {
|
||||
stop-services $args
|
||||
}
|
||||
platform-status
|
||||
} else {
|
||||
# No service specified - stop all
|
||||
print "🛑 Stopping All Platform Services"
|
||||
print "═════════════════════════════════"
|
||||
print ""
|
||||
|
||||
# Kill all provisioning service binaries (excluding this CLI)
|
||||
(^sh -c "pkill -f 'provisioning-[a-z]' 2>/dev/null || true") | ignore
|
||||
ncl-sync-stop
|
||||
sleep 500ms
|
||||
print "✓ All services stopped"
|
||||
print ""
|
||||
|
||||
# Show updated status
|
||||
platform-status
|
||||
}
|
||||
}
|
||||
|
||||
def platform-restart [args: list<string>, flags: record] {
|
||||
print ""
|
||||
|
||||
# If a service name is provided, restart only that service
|
||||
if ($args | length) > 0 {
|
||||
let service_name = $args | get 0
|
||||
|
||||
# ncl-sync doesn't follow the provisioning-* binary convention — handle separately.
|
||||
if $service_name == "ncl-sync" or $service_name == "ncl_sync" {
|
||||
ncl-sync-stop
|
||||
sleep 500ms
|
||||
ncl-sync-start
|
||||
print ""
|
||||
platform-status
|
||||
return
|
||||
}
|
||||
|
||||
# Normalize: strip "provisioning-" prefix so both "daemon" and "provisioning-daemon" work
|
||||
let svc_short = ($service_name | str replace "_" "-"
|
||||
| if ($in | str starts-with "provisioning-") { str replace "provisioning-" "" } else { $in })
|
||||
let binary_name = $"provisioning-($svc_short)"
|
||||
let port = (get-service-port $svc_short)
|
||||
|
||||
# Stop the service
|
||||
(^sh -c $"pkill -f '($binary_name)' 2>/dev/null || true") | ignore
|
||||
sleep 1sec
|
||||
|
||||
# Start the service
|
||||
print $"→ Starting ($service_name)..."
|
||||
|
||||
let home = ($env.HOME? | default "~" | path expand)
|
||||
let canonical_path = ($home | path join ".local/share/provisioning/bin" $binary_name)
|
||||
let binary_path = if ($canonical_path | path exists) { $canonical_path } else { $home | path join ".local/bin" $binary_name }
|
||||
|
||||
if not ($binary_path | path exists) {
|
||||
print $"✗ Binary not found: ($binary_path)"
|
||||
return
|
||||
}
|
||||
|
||||
let log_dir = ($home | path join ".provisioning/logs")
|
||||
(do { mkdir ($log_dir) } | ignore)
|
||||
let log_file = ($log_dir | path join $"($service_name).log")
|
||||
|
||||
# Set environment variables for service
|
||||
let platform_path = (get-platform-config-dir)
|
||||
|
||||
# Properly quote environment variables for shell execution
|
||||
let start_cmd = $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &"
|
||||
(^sh -c $start_cmd | ignore)
|
||||
sleep 2sec
|
||||
|
||||
let started_msg = $"((ansi green))started((ansi reset))"
|
||||
print $"✓ ($service_name) on port ($port) — ($started_msg)"
|
||||
print ""
|
||||
|
||||
# Show updated status
|
||||
platform-status
|
||||
} else {
|
||||
# No service specified - restart all
|
||||
print "🔄 Restarting All Platform Services"
|
||||
print "═════════════════════════════════"
|
||||
print ""
|
||||
|
||||
# Stop all
|
||||
(^sh -c "pkill -f 'provisioning-[a-z]' 2>/dev/null || true") | ignore
|
||||
print "✓ All services stopped"
|
||||
|
||||
# Wait
|
||||
sleep 2sec
|
||||
|
||||
# Start all
|
||||
start-required-services
|
||||
|
||||
# Wait for services to initialize
|
||||
sleep 1sec
|
||||
|
||||
# Show updated status
|
||||
platform-status
|
||||
}
|
||||
}
|
||||
|
||||
def platform-status [args: list<string> = []] {
|
||||
let json_out = ("--json" in $args)
|
||||
let clean_args = ($args | where { |a| $a != "--json" })
|
||||
let filter = ($clean_args | get 0? | default null)
|
||||
let filter_key = if $filter != null { $filter | str replace --all "-" "_" } else { null }
|
||||
|
||||
let deployment = (load-deployment-mode)
|
||||
let all_services = (
|
||||
if ($deployment | get --optional "services") != null { $deployment.services | columns } else { [] }
|
||||
)
|
||||
|
||||
if ($all_services | length) == 0 and not $json_out {
|
||||
print "⚠ No services found in deployment configuration"
|
||||
return
|
||||
}
|
||||
|
||||
# ── Platform services ────────────────────────────────────────────────────────
|
||||
let plat_data = $all_services | each { |service_name|
|
||||
let config = $deployment.services | get $service_name
|
||||
let enabled = ($config | get "enabled" | default false)
|
||||
let port = (get-service-port $service_name)
|
||||
let port_num = (if $port != "?" { $port | into int } else { 0 })
|
||||
let running = (
|
||||
if $port_num > 0 { is-port-listening $port_num }
|
||||
else {
|
||||
let bin = $"provisioning-(normalize-service-name $service_name | str replace '_' '-')"
|
||||
(do { ^pgrep -f $bin } | complete).exit_code == 0
|
||||
}
|
||||
)
|
||||
{ kind: "platform", name: $service_name, port: $port, enabled: $enabled, running: $running, required: $enabled, url: "" }
|
||||
}
|
||||
|
||||
let ncs = (ncl-sync-status)
|
||||
let plat_with_ncs = ($plat_data | append {
|
||||
kind: "platform", name: "ncl_sync", port: "—", enabled: true, running: $ncs.running, required: false, url: ""
|
||||
})
|
||||
|
||||
# ── External services ────────────────────────────────────────────────────────
|
||||
let ext_data = (get-external-services) | each { |svc|
|
||||
let running = (is-port-listening $svc.port)
|
||||
{ kind: "external", name: $svc.name, port: ($svc.port | into string), enabled: true, running: $running, required: ($svc.required? | default false), url: $svc.url }
|
||||
}
|
||||
|
||||
let all_data = ($plat_with_ncs | append $ext_data)
|
||||
|
||||
# ── Filter by service name ───────────────────────────────────────────────────
|
||||
let display_data = if $filter_key != null {
|
||||
let matched = ($all_data | where { |r| ($r.name | str replace --all "-" "_") == $filter_key })
|
||||
if ($matched | is-empty) {
|
||||
if $json_out { print ({error: $"unknown service: ($filter)"} | to json); return }
|
||||
print $"❌ Unknown service: ($filter)"; return
|
||||
}
|
||||
$matched
|
||||
} else {
|
||||
$all_data
|
||||
}
|
||||
|
||||
# ── JSON output ──────────────────────────────────────────────────────────────
|
||||
if $json_out {
|
||||
print ($display_data | to json)
|
||||
return
|
||||
}
|
||||
|
||||
# ── Human output ─────────────────────────────────────────────────────────────
|
||||
let table_rows = $display_data | each { |r|
|
||||
let status_str = if $r.running {
|
||||
let label = if $r.enabled { "running" } else { "running*" }
|
||||
$"((ansi green))($label)((ansi reset))"
|
||||
} else {
|
||||
let label = if $r.enabled { "stopped" } else { "disabled" }
|
||||
if $r.enabled { $"((ansi red))($label)((ansi reset))" } else { $"((ansi dark_gray))($label)((ansi reset))" }
|
||||
}
|
||||
let req_str = if $r.required { $"((ansi yellow))req((ansi reset))" } else { "opt" }
|
||||
{ Kind: $r.kind, Service: $r.name, Status: $status_str, Port: $r.port, Req: $req_str }
|
||||
}
|
||||
|
||||
print ""
|
||||
if $filter_key != null {
|
||||
print $"📊 Status: ($filter)"
|
||||
} else {
|
||||
print "📊 Platform + External Services"
|
||||
print "════════════════════════════════"
|
||||
}
|
||||
print ""
|
||||
print ($table_rows | table -i false)
|
||||
print ""
|
||||
|
||||
if $filter_key == null {
|
||||
let running = ($display_data | where running | length)
|
||||
let req_down = ($display_data | where { |r| $r.required and (not $r.running) } | length)
|
||||
print $" ✓ Running: ($running) / ($display_data | length)"
|
||||
if $req_down > 0 { print $" ❌ Required down: ($req_down) — run: prvng plat start" }
|
||||
print ""
|
||||
print "Legend: req=required opt=optional *=running but disabled in config"
|
||||
}
|
||||
}
|
||||
|
||||
def platform-external [] {
|
||||
print ""
|
||||
print "🔧 External Services (Infrastructure)"
|
||||
print "════════════════════════════════════"
|
||||
print ""
|
||||
|
||||
let external_services = (get-external-services)
|
||||
|
||||
if ($external_services | length) == 0 {
|
||||
print "No external services configured"
|
||||
return
|
||||
}
|
||||
|
||||
# Build external services table
|
||||
let external_data = [
|
||||
...($external_services | each { |service|
|
||||
let name = ($service | get "name")
|
||||
let url = ($service | get "url")
|
||||
let port = ($service | get "port")
|
||||
let required = ($service | get "required" | default false)
|
||||
let dependencies = ($service | get "dependencies" | default [] | str join ", ")
|
||||
|
||||
# Check if service is running by testing if port is listening
|
||||
let is_running = (is-port-listening $port)
|
||||
|
||||
let status = (
|
||||
if $is_running {
|
||||
$"((ansi green))running((ansi reset))"
|
||||
} else {
|
||||
$"((ansi red))stopped((ansi reset))"
|
||||
}
|
||||
)
|
||||
|
||||
let required_display = (
|
||||
if $required {
|
||||
$"((ansi red))required((ansi reset))"
|
||||
} else {
|
||||
$"((ansi dark_gray))optional((ansi reset))"
|
||||
}
|
||||
)
|
||||
|
||||
{
|
||||
Service: $name,
|
||||
URL: $url,
|
||||
Port: $port,
|
||||
Status: $status,
|
||||
Dependencies: $dependencies,
|
||||
Required: $required_display
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
# Display external services table
|
||||
print ($external_data | table -i false)
|
||||
|
||||
print ""
|
||||
let external_running = (
|
||||
$external_data
|
||||
| where { |row| ($row.Status | str contains "running") }
|
||||
| length
|
||||
)
|
||||
let external_total = ($external_data | length)
|
||||
let external_stopped = ($external_total - $external_running)
|
||||
|
||||
print "Summary:"
|
||||
print $" ✓ Running: ($external_running)"
|
||||
print $" ✗ Stopped: ($external_stopped)"
|
||||
print ""
|
||||
print "Note: External services are monitored only. Use system commands to manage them."
|
||||
}
|
||||
|
||||
def platform-health [] {
|
||||
print ""
|
||||
print "💚 Platform Services Health Check"
|
||||
print "═════════════════════════════════"
|
||||
print ""
|
||||
|
||||
let deployment = (load-deployment-mode)
|
||||
let all_services = $deployment.services | columns
|
||||
mut healthy_count = 0
|
||||
mut critical_services = []
|
||||
|
||||
for service_name in $all_services {
|
||||
let config = $deployment.services | get $service_name
|
||||
let enabled = ($config | get "enabled" | default false)
|
||||
|
||||
# Load individual service config to get the actual port
|
||||
let port = (get-service-port $service_name)
|
||||
|
||||
let binary_name = $"provisioning-($service_name | str replace "_" "-")"
|
||||
|
||||
let is_running = ((^pgrep -f $"[/]($binary_name)$" | complete).exit_code == 0)
|
||||
|
||||
if $is_running {
|
||||
print $" ✓ ($service_name) — healthy on port ($port)"
|
||||
$healthy_count = ($healthy_count + 1)
|
||||
} else if $enabled {
|
||||
print $" ✗ ($service_name) — CRITICAL \(enabled but not running\)"
|
||||
$critical_services = ($critical_services | append $service_name)
|
||||
} else {
|
||||
print $" ⊘ ($service_name) — disabled"
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
print "Health Summary:"
|
||||
let total = ($all_services | length)
|
||||
let critical_count = ($critical_services | length)
|
||||
print $" ✓ Running: ($healthy_count) / ($total)"
|
||||
print $" ⚠️ Critical: ($critical_count)"
|
||||
|
||||
if ($critical_count == 0) {
|
||||
print $" Status: ✅ All enabled services are running"
|
||||
} else {
|
||||
print $" Status: ⚠️ Missing critical services:"
|
||||
for svc in $critical_services {
|
||||
print $" - ($svc)"
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
}
|
||||
|
||||
def platform-list [] {
|
||||
print ""
|
||||
print "📋 Available Platform Services"
|
||||
print "═════════════════════════════"
|
||||
print ""
|
||||
|
||||
let services_info = [
|
||||
{name: "vault-service", port: 8081, protocol: "gRPC", deps: "none"}
|
||||
{name: "catalog-registry", port: 8082, protocol: "HTTP", deps: "none"}
|
||||
{name: "control-center", port: 8000, protocol: "HTTP/WebSocket", deps: "vault-service"}
|
||||
{name: "provisioning-rag", port: 8300, protocol: "REST", deps: "none"}
|
||||
{name: "ai-service", port: 8083, protocol: "HTTP", deps: "provisioning-rag, vault-service"}
|
||||
{name: "mcp-server", port: 8400, protocol: "Binary", deps: "vault-service"}
|
||||
{name: "provisioning-nu-daemon", port: 8100, protocol: "gRPC", deps: "vault-service"}
|
||||
{name: "orchestrator", port: 9090, protocol: "HTTP", deps: "catalog-registry, control-center, ai-service"}
|
||||
{name: "detector", port: 8600, protocol: "HTTP", deps: "vault-service"}
|
||||
{name: "control-center-ui", port: 3000, protocol: "HTTP (WASM)", deps: "control-center"}
|
||||
]
|
||||
|
||||
for svc in $services_info {
|
||||
print $" • ($svc.name)"
|
||||
print $" Port: ($svc.port), Protocol: ($svc.protocol)"
|
||||
print $" Dependencies: ($svc.deps)"
|
||||
print ""
|
||||
}
|
||||
|
||||
print "Total: 10 services"
|
||||
print ""
|
||||
}
|
||||
|
||||
def platform-config [] {
|
||||
print ""
|
||||
print "⚙️ Platform Configuration"
|
||||
print "════════════════════════"
|
||||
print ""
|
||||
|
||||
let platform_base = ($env.PROVISIONING_USER_PLATFORM? | default "~/.config/provisioning/platform")
|
||||
print $"Platform Directory: ($platform_base)"
|
||||
print ""
|
||||
|
||||
print "Configuration Files:"
|
||||
print $" • ($platform_base)/deployment-mode.ncl"
|
||||
print $" • ($platform_base)/config/control-center.ncl"
|
||||
print $" • ($platform_base)/config/orchestrator.ncl"
|
||||
print ""
|
||||
}
|
||||
|
||||
def platform-connections [] {
|
||||
print ""
|
||||
print "🔗 Platform Service Connections"
|
||||
print "════════════════════════════════"
|
||||
print ""
|
||||
|
||||
print "Service Dependency Graph:"
|
||||
print " vault-service"
|
||||
print " ↓"
|
||||
print " ├─ control-center"
|
||||
print " │ ↓"
|
||||
print " │ orchestrator"
|
||||
print " │"
|
||||
print " ├─ ai-service"
|
||||
print " │ ↓"
|
||||
print " │ orchestrator"
|
||||
print " │"
|
||||
print " ├─ mcp-server"
|
||||
print " └─ provisioning-nu-daemon"
|
||||
print ""
|
||||
|
||||
print "Service Network Endpoints:"
|
||||
print " • vault-service: grpc://localhost:8081"
|
||||
print " • catalog-registry: http://localhost:8082"
|
||||
print " • control-center: http://localhost:8000"
|
||||
print " • provisioning-rag: http://localhost:8300"
|
||||
print " • ai-service: http://localhost:8083"
|
||||
print " • mcp-server: grpc://localhost:8400"
|
||||
print " • provisioning-nu-daemon: grpc://localhost:8100"
|
||||
print " • orchestrator: http://localhost:9011"
|
||||
print " • detector: http://localhost:8600"
|
||||
print " • control-center-ui: http://localhost:3000"
|
||||
print ""
|
||||
}
|
||||
|
||||
def platform-init [] {
|
||||
print ""
|
||||
print "🔧 Platform Initialization"
|
||||
print "═════════════════════════"
|
||||
print ""
|
||||
|
||||
let platform_base = ($env.PROVISIONING_USER_PLATFORM? | default "~/.config/provisioning/platform")
|
||||
print $"Platform Directory: ($platform_base)"
|
||||
print ""
|
||||
|
||||
if ($"($platform_base)" | path exists) {
|
||||
print "✓ Platform directory exists"
|
||||
} else {
|
||||
print "⚠ Platform directory not found"
|
||||
print " Run: setup-platform-config.sh to initialize"
|
||||
}
|
||||
|
||||
print ""
|
||||
print "Platform is ready for:"
|
||||
print " • provisioning platform start local - Start local services"
|
||||
print " • provisioning platform status - Check service status"
|
||||
print " • provisioning platform health - Health checks"
|
||||
print " • provisioning platform list - List services"
|
||||
print ""
|
||||
}
|
||||
|
||||
def platform-logs [args: list<string>] {
|
||||
let home = ($env.HOME? | default "~" | path expand)
|
||||
let log_dir = ($home | path join ".provisioning" "logs")
|
||||
|
||||
# Parse args: first non-numeric token = service name, first numeric token = lines limit
|
||||
let service_arg = ($args | where { |a| not ($a =~ '^[0-9]+$') } | get 0?)
|
||||
let lines_raw = ($args | where { |a| $a =~ '^[0-9]+$' } | get 0?)
|
||||
let lines_arg = if $lines_raw != null { $lines_raw | into int } else { null }
|
||||
|
||||
if not ($log_dir | path exists) {
|
||||
print "❌ Log directory not found: ~/.provisioning/logs"
|
||||
print " Start services first: provisioning platform start"
|
||||
return
|
||||
}
|
||||
|
||||
# Resolve initial log file when service name provided upfront
|
||||
let resolved_initial = if $service_arg != null {
|
||||
let exact = ($log_dir | path join $"($service_arg).log")
|
||||
let under = ($log_dir | path join $"($service_arg | str replace --all '-' '_').log")
|
||||
if ($exact | path exists) {
|
||||
$exact
|
||||
} else if ($under | path exists) {
|
||||
$under
|
||||
} else {
|
||||
print ""
|
||||
print $"❌ No log file for: ($service_arg)"
|
||||
print $" Tried: ($exact)"
|
||||
print $" Tried: ($under)"
|
||||
print $" Start with: provisioning platform start ($service_arg)"
|
||||
print ""
|
||||
return
|
||||
}
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
mut keep_going = true
|
||||
mut current_log = $resolved_initial
|
||||
|
||||
while $keep_going {
|
||||
# Resolve log file for this iteration: preselected or interactive selector
|
||||
let log_file = if ($current_log | is-not-empty) {
|
||||
$current_log
|
||||
} else {
|
||||
let entries = (
|
||||
ls ($log_dir)
|
||||
| where type == file
|
||||
| where name =~ '\.log$'
|
||||
| get name
|
||||
| each { |f| $f | path basename | str replace --regex '\.log$' '' }
|
||||
)
|
||||
if ($entries | length) == 0 {
|
||||
print "❌ No log files in ~/.provisioning/logs"
|
||||
$keep_going = false
|
||||
""
|
||||
} else {
|
||||
let selected = (typedialog select "Service logs:" $entries)
|
||||
$log_dir | path join $"($selected).log"
|
||||
}
|
||||
}
|
||||
|
||||
if $keep_going and ($log_file | is-not-empty) {
|
||||
let label = ($log_file | path basename | str replace --regex '\.log$' '')
|
||||
print ""
|
||||
print $"📋 ($label)"
|
||||
print "─────────────────────────────────────────────────"
|
||||
print ""
|
||||
|
||||
if $lines_arg != null {
|
||||
^tail -n $lines_arg ($log_file)
|
||||
} else {
|
||||
^cat ($log_file)
|
||||
}
|
||||
|
||||
print ""
|
||||
print "─────────────────────────────────────────────────"
|
||||
print $" ($log_file)"
|
||||
print ""
|
||||
|
||||
let choice = (typedialog select "¿Qué deseas hacer?" ["Ver otro log" "Salir"])
|
||||
$current_log = ""
|
||||
if $choice == "Salir" {
|
||||
$keep_going = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def platform-check [] {
|
||||
print ""
|
||||
print "🔍 Checking External Services"
|
||||
print "════════════════════════════"
|
||||
print ""
|
||||
|
||||
# For now, provide template for checking external services
|
||||
# TODO: Load actual config from external-services config file
|
||||
|
||||
print "External Services to Check:"
|
||||
print " ✓ Database (SurrealDB/PostgreSQL/Filesystem)"
|
||||
print " ✓ OCI Registry (Zot/Harbor) for extensions"
|
||||
print " ✓ Git Source (Forgejo/Gitea/GitHub) for discovery"
|
||||
print " ✓ Cache Service (Local directory or Redis)"
|
||||
print ""
|
||||
|
||||
print "To implement full checks, ensure config is loaded from:"
|
||||
print " • PLATFORM_MODE environment variable"
|
||||
print " • Workspace config/platform/deployment-mode.ncl"
|
||||
print " • System defaults"
|
||||
print ""
|
||||
|
||||
print "Remediation:"
|
||||
print " 1. Set deployment mode: export PLATFORM_MODE=solo|multiuser|enterprise"
|
||||
print " 2. Configure external services in platform config"
|
||||
print " 3. Run 'provisioning platform check' again"
|
||||
print ""
|
||||
}
|
||||
|
||||
def show-platform-help [] {
|
||||
print ""
|
||||
print "🖥️ Platform Commands"
|
||||
print "===================="
|
||||
print ""
|
||||
print " platform start [mode|service] - Start services (mode from deployment-mode.ncl if omitted)"
|
||||
print " platform stop [service] - Stop all services or specific service"
|
||||
print " platform restart [service] - Restart all services or specific service"
|
||||
print " platform status - Show service status"
|
||||
print " platform health - Health checks"
|
||||
print " platform external - Show external services status"
|
||||
print " platform list - List available services"
|
||||
print " platform config - Show configuration"
|
||||
print " platform connections - Show service connections"
|
||||
print " platform logs [service] - Stream service logs (interactive selector if no service given)"
|
||||
print " platform init - Initialize platform"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning platform start # Start using deployment-mode"
|
||||
print " provisioning platform start local --services core # Override mode"
|
||||
print " provisioning platform start vault_service # Start single service"
|
||||
print " provisioning platform stop orchestrator # Stop single service"
|
||||
print " provisioning platform restart vault_service # Restart single service"
|
||||
print " provisioning platform status"
|
||||
print " provisioning platform health"
|
||||
print " provisioning platform external # Check external services"
|
||||
print " provisioning platform logs # Interactive selector → show full log → loop"
|
||||
print " provisioning platform logs orchestrator # Show full orchestrator log"
|
||||
print " provisioning platform logs orchestrator 50 # Show last 50 lines"
|
||||
print " prvng p logs 100 # Interactive selector, last 100 lines"
|
||||
print ""
|
||||
}
|
||||
173
nulib/cli/handlers/runner_daemon.nu
Normal file
173
nulib/cli/handlers/runner_daemon.nu
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# Runner daemon handler — list/destroy/pin/unpin/status via lian-build daemon HTTP API.
|
||||
|
||||
const DAEMON_DEFAULT_URL = "http://localhost:14150"
|
||||
|
||||
def daemon-url []: nothing -> string {
|
||||
$env.LIAN_BUILD_DAEMON_URL? | default $DAEMON_DEFAULT_URL
|
||||
}
|
||||
|
||||
def format-runner [r: record]: nothing -> record {
|
||||
let ttl = if $r.lifecycle.ttl_minutes == 0 { "—" } else { $"($r.lifecycle.ttl_minutes)m" }
|
||||
let sched = $r.lifecycle.destroy_at? | default "—"
|
||||
{
|
||||
id: ($r.id | str substring 0..<8)
|
||||
workspace: $r.workspace
|
||||
host: $"($r.ssh_host):($r.ssh_port)"
|
||||
status: $r.status
|
||||
pin: $r.lifecycle.pin
|
||||
ttl: $ttl
|
||||
destroy_at: $sched
|
||||
spawned_at: $r.spawned_at
|
||||
}
|
||||
}
|
||||
|
||||
def runner-list [base_url: string]: nothing -> nothing {
|
||||
let rows = try {
|
||||
http get $"($base_url)/runners"
|
||||
} catch {
|
||||
print $"error: daemon unreachable at ($base_url)"
|
||||
print " Start with: lian-build serve"
|
||||
return
|
||||
}
|
||||
|
||||
if ($rows | is-empty) {
|
||||
print "no active runners"
|
||||
return
|
||||
}
|
||||
|
||||
$rows | each { |r| format-runner $r } | table
|
||||
}
|
||||
|
||||
def runner-status [base_url: string, id: string]: nothing -> nothing {
|
||||
if ($id | is-empty) {
|
||||
print "error: runner status requires an id (full UUID or 8-char prefix)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let rows = try {
|
||||
http get $"($base_url)/runners"
|
||||
} catch {
|
||||
print $"error: daemon unreachable at ($base_url)"
|
||||
print " Start with: lian-build serve"
|
||||
return
|
||||
}
|
||||
|
||||
let matches = $rows | where { |r| ($r.id | str starts-with $id) }
|
||||
|
||||
if ($matches | is-empty) {
|
||||
print $"error: no runner matching id '($id)'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$matches | first | table
|
||||
}
|
||||
|
||||
def runner-destroy [base_url: string, id: string, confirmed: bool]: nothing -> nothing {
|
||||
if not $confirmed {
|
||||
print $"Destroy runner ($id)? This cannot be undone."
|
||||
let answer = (input "Confirm [y/N]: " | str downcase | str trim)
|
||||
if $answer != "y" {
|
||||
print "aborted"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let result = try {
|
||||
http delete $"($base_url)/runners/($id)"
|
||||
{ ok: true }
|
||||
} catch { |e|
|
||||
{ ok: false, error: ($e | get msg? | default "request failed") }
|
||||
}
|
||||
|
||||
if $result.ok {
|
||||
print $"runner ($id) queued for destruction"
|
||||
} else {
|
||||
print $"error: ($result.error)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
def runner-set-pin [base_url: string, id: string, pin: bool]: nothing -> nothing {
|
||||
let endpoint = if $pin { "pin" } else { "unpin" }
|
||||
|
||||
let result = try {
|
||||
http post $"($base_url)/runners/($id)/($endpoint)" {}
|
||||
{ ok: true }
|
||||
} catch { |e|
|
||||
{ ok: false, error: ($e | get msg? | default "request failed") }
|
||||
}
|
||||
|
||||
if $result.ok {
|
||||
let verb = if $pin { "pinned" } else { "unpinned" }
|
||||
print $"runner ($id) ($verb)"
|
||||
} else {
|
||||
print $"error: ($result.error)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
def runner-help []: nothing -> nothing {
|
||||
print "Runner Daemon Commands"
|
||||
print "======================"
|
||||
print ""
|
||||
print "Usage: provisioning runner <command> [options]"
|
||||
print ""
|
||||
print "Commands:"
|
||||
print " list List active runners"
|
||||
print " status <id> Show full record for a runner (prefix match)"
|
||||
print " destroy <id> Destroy a runner (prompts unless --yes)"
|
||||
print " pin <id> Pin runner — daemon will not auto-destroy it"
|
||||
print " unpin <id> Unpin runner — daemon resumes TTL/schedule enforcement"
|
||||
print ""
|
||||
print "Environment:"
|
||||
print $" LIAN_BUILD_DAEMON_URL Daemon base URL (default: ($DAEMON_DEFAULT_URL))"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning runner list"
|
||||
print " provisioning runner status a1b2c3d4"
|
||||
print " provisioning runner destroy a1b2c3d4 --yes"
|
||||
print " provisioning runner pin a1b2c3d4"
|
||||
}
|
||||
|
||||
export def handle_runner_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
]: nothing -> nothing {
|
||||
let url = daemon-url
|
||||
let id = ($ops | str trim)
|
||||
let confirmed = ($flags.auto_confirm? | default false)
|
||||
|
||||
match $command {
|
||||
"list" | "l" => {
|
||||
runner-list $url
|
||||
}
|
||||
"status" | "s" => {
|
||||
runner-status $url $id
|
||||
}
|
||||
"destroy" | "d" => {
|
||||
if ($id | is-empty) {
|
||||
print "error: runner destroy requires an id"
|
||||
exit 1
|
||||
}
|
||||
runner-destroy $url $id $confirmed
|
||||
}
|
||||
"pin" | "p" => {
|
||||
if ($id | is-empty) {
|
||||
print "error: runner pin requires an id"
|
||||
exit 1
|
||||
}
|
||||
runner-set-pin $url $id true
|
||||
}
|
||||
"unpin" | "u" => {
|
||||
if ($id | is-empty) {
|
||||
print "error: runner unpin requires an id"
|
||||
exit 1
|
||||
}
|
||||
runner-set-pin $url $id false
|
||||
}
|
||||
_ => {
|
||||
runner-help
|
||||
}
|
||||
}
|
||||
}
|
||||
458
nulib/cli/handlers/secretumvault.nu
Normal file
458
nulib/cli/handlers/secretumvault.nu
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# SecretumVault Command Handlers
|
||||
# Handles: kms encrypt, kms decrypt, kms generate-key, kms health, kms version, kms rotate-key
|
||||
|
||||
use cli/flags.nu *
|
||||
use platform/plugins_lib/secretumvault.nu *
|
||||
|
||||
# Main SecretumVault command dispatcher
|
||||
export def handle_secretumvault_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
match $command {
|
||||
"secretumvault" | "sv" | "vault" => {
|
||||
let subcommand = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
"help"
|
||||
}
|
||||
|
||||
let remaining_ops = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | skip 1 | str join " ")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
match $subcommand {
|
||||
"encrypt" => { handle_sv_encrypt $remaining_ops $flags }
|
||||
"decrypt" => { handle_sv_decrypt $remaining_ops $flags }
|
||||
"generate-key" | "generate" | "gen-key" => { handle_sv_generate_key $remaining_ops $flags }
|
||||
"encrypt-file" | "enc-file" => { handle_sv_encrypt_file $remaining_ops $flags }
|
||||
"decrypt-file" | "dec-file" => { handle_sv_decrypt_file $remaining_ops $flags }
|
||||
"rotate-key" | "rotate" => { handle_sv_rotate_key $remaining_ops $flags }
|
||||
"health" | "check" => { handle_sv_health $flags }
|
||||
"version" | "ver" => { handle_sv_version $flags }
|
||||
"status" | "info" => { handle_sv_status $flags }
|
||||
"help" => { show_sv_help }
|
||||
_ => {
|
||||
print $"❌ Unknown SecretumVault subcommand: ($subcommand)"
|
||||
print ""
|
||||
print "Available SecretumVault subcommands:"
|
||||
print " encrypt - Encrypt data"
|
||||
print " decrypt - Decrypt data"
|
||||
print " generate-key - Generate new encryption key"
|
||||
print " encrypt-file - Encrypt configuration file"
|
||||
print " decrypt-file - Decrypt configuration file"
|
||||
print " rotate-key - Rotate encryption key"
|
||||
print " health - Check service health"
|
||||
print " version - Get version information"
|
||||
print " status - Show plugin status and configuration"
|
||||
print " help - Show this help message"
|
||||
print ""
|
||||
print "Use 'provisioning secretumvault help' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown SecretumVault command: ($command)"
|
||||
print "Use 'provisioning secretumvault help' for available commands"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Encrypt plaintext data
|
||||
def handle_sv_encrypt [ops: string, flags: record] {
|
||||
let plaintext = if ($flags.data? | is-not-empty) {
|
||||
$flags.data
|
||||
} else if ($flags.plaintext? | is-not-empty) {
|
||||
$flags.plaintext
|
||||
} else if ($ops | is-not-empty) {
|
||||
$ops
|
||||
} else {
|
||||
print $"❌ Error: plaintext data required"
|
||||
print "Usage: provisioning secretumvault encrypt <plaintext> [--key-id <key>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let key_id = if ($flags.key_id? | is-not-empty) { $flags.key_id } else { "" }
|
||||
|
||||
print $"Encrypting data..."
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
plugin-secretumvault-encrypt $plaintext
|
||||
} else {
|
||||
plugin-secretumvault-encrypt $plaintext --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Encryption successful\n"
|
||||
|
||||
if ($result | type) == "record" {
|
||||
print $"Key ID: ($result.key_id? | default 'N/A')"
|
||||
print $"Algorithm: ($result.algorithm? | default 'AES-256-GCM')"
|
||||
print $"Ciphertext:"
|
||||
print $result.ciphertext
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $"❌ Encryption failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Decrypt ciphertext data
|
||||
def handle_sv_decrypt [ops: string, flags: record] {
|
||||
let ciphertext = if ($flags.ciphertext? | is-not-empty) {
|
||||
$flags.ciphertext
|
||||
} else if ($ops | is-not-empty) {
|
||||
$ops
|
||||
} else {
|
||||
print $"❌ Error: ciphertext required"
|
||||
print "Usage: provisioning secretumvault decrypt <ciphertext> [--key-id <key>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let key_id = if ($flags.key_id? | is-not-empty) { $flags.key_id } else { "" }
|
||||
|
||||
print $"Decrypting data..."
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
plugin-secretumvault-decrypt $ciphertext
|
||||
} else {
|
||||
plugin-secretumvault-decrypt $ciphertext --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Decryption successful\n"
|
||||
|
||||
if ($result | type) == "record" {
|
||||
if ($result.plaintext? | is-not-empty) {
|
||||
print $"Plaintext:"
|
||||
print $result.plaintext
|
||||
print $"Key ID: ($result.key_id? | default 'N/A')"
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $"❌ Decryption failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Generate new data key
|
||||
def handle_sv_generate_key [ops: string, flags: record] {
|
||||
let bits_input = if ($flags.bits? | is-not-empty) { $flags.bits } else { "" }
|
||||
let bits = if ($bits_input | is-empty) {
|
||||
256
|
||||
} else {
|
||||
let conversion = (do { $bits_input | into int } | complete)
|
||||
if $conversion.exit_code == 0 { $conversion.stdout } else { 256 }
|
||||
}
|
||||
|
||||
let key_id = if ($flags.key_id? | is-not-empty) { $flags.key_id } else { "" }
|
||||
|
||||
print $"Generating data key ($bits) bits..."
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
plugin-secretumvault-generate-key --bits $bits
|
||||
} else {
|
||||
plugin-secretumvault-generate-key --bits $bits --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Key generation successful\n"
|
||||
|
||||
if ($result | type) == "record" {
|
||||
print $"Key Size: ($result.bits? | default $bits) bits"
|
||||
print $"Algorithm: ($result.algorithm? | default 'AES')"
|
||||
print $"Key ID: ($result.key_id? | default 'N/A')"
|
||||
print $"Plaintext Key:"
|
||||
print $result.plaintext
|
||||
print ""
|
||||
print $"Encrypted Key:"
|
||||
print $result.ciphertext
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $"❌ Key generation failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Encrypt configuration file
|
||||
def handle_sv_encrypt_file [ops: string, flags: record] {
|
||||
let file = if ($flags.file? | is-not-empty) {
|
||||
$flags.file
|
||||
} else if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
print $"❌ Error: file path required"
|
||||
print "Usage: provisioning secretumvault encrypt-file <file> [--output <path>] [--key-id <key>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if not ($file | path exists) {
|
||||
print $"❌ Error: file not found: ($file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let output = if ($flags.output? | is-not-empty) {
|
||||
$flags.output
|
||||
} else {
|
||||
$"($file).enc"
|
||||
}
|
||||
|
||||
let key_id = if ($flags.key_id? | is-not-empty) { $flags.key_id } else { "" }
|
||||
|
||||
print $"Encrypting file: ($file)"
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
encrypt-config-file $file --output $output
|
||||
} else {
|
||||
encrypt-config-file $file --output $output --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
if ($result.success? | default false) {
|
||||
print $"✓ File encrypted successfully\n"
|
||||
print $"Input: ($result.input_file? | default $file)"
|
||||
print $"Output: ($result.output_file? | default $output)"
|
||||
print $"Key ID: ($result.key_id? | default 'N/A')"
|
||||
} else {
|
||||
print $"❌ File encryption failed"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
print $"❌ File encryption failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Decrypt configuration file
|
||||
def handle_sv_decrypt_file [ops: string, flags: record] {
|
||||
let file = if ($flags.file? | is-not-empty) {
|
||||
$flags.file
|
||||
} else if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
print $"❌ Error: file path required"
|
||||
print "Usage: provisioning secretumvault decrypt-file <file> [--output <path>] [--key-id <key>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if not ($file | path exists) {
|
||||
print $"❌ Error: file not found: ($file)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let output = if ($flags.output? | is-not-empty) {
|
||||
$flags.output
|
||||
} else {
|
||||
let base_name = ($file | str replace '.enc' '')
|
||||
$"($base_name).dec"
|
||||
}
|
||||
|
||||
let key_id = if ($flags.key_id? | is-not-empty) { $flags.key_id } else { "" }
|
||||
|
||||
print $"Decrypting file: ($file)"
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
decrypt-config-file $file --output $output
|
||||
} else {
|
||||
decrypt-config-file $file --output $output --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
if ($result.success? | default false) {
|
||||
print $"✓ File decrypted successfully\n"
|
||||
print $"Input: ($result.input_file? | default $file)"
|
||||
print $"Output: ($result.output_file? | default $output)"
|
||||
print $"Key ID: ($result.key_id? | default 'N/A')"
|
||||
} else {
|
||||
print $"❌ File decryption failed"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
print $"❌ File decryption failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Rotate encryption key
|
||||
def handle_sv_rotate_key [ops: string, flags: record] {
|
||||
let key_id = if ($flags.key_id? | is-not-empty) {
|
||||
$flags.key_id
|
||||
} else if ($ops | is-not-empty) {
|
||||
$ops
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
print $"Rotating encryption key..."
|
||||
|
||||
let result = (do -i {
|
||||
if ($key_id | is-empty) {
|
||||
plugin-secretumvault-rotate-key
|
||||
} else {
|
||||
plugin-secretumvault-rotate-key --key-id $key_id
|
||||
}
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Key rotation successful\n"
|
||||
|
||||
if ($result | type) == "record" {
|
||||
print $"Status: ($result.status? | default 'Success')"
|
||||
print $"Message: ($result.message? | default 'Key rotated successfully')"
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $"❌ Key rotation failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Check service health
|
||||
def handle_sv_health [flags: record] {
|
||||
print $"Checking SecretumVault health..."
|
||||
|
||||
let result = (do -i {
|
||||
plugin-secretumvault-health
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Health check complete\n"
|
||||
|
||||
if ($result | type) == "record" {
|
||||
let healthy = ($result.healthy? | default false)
|
||||
let health_status = if $healthy { $"✓ Healthy" } else { $"✗ Unhealthy" }
|
||||
|
||||
print $"Status: $health_status"
|
||||
print $"Status Code: ($result.status_code? | default 'N/A')"
|
||||
print $"Version: ($result.version? | default 'unknown')"
|
||||
print $"Initialized: ($result.initialized? | default false)"
|
||||
print $"Sealed: ($result.sealed? | default true)"
|
||||
} else {
|
||||
print $result
|
||||
}
|
||||
} else {
|
||||
print $"❌ Health check failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Get version information
|
||||
def handle_sv_version [flags: record] {
|
||||
print $"Getting SecretumVault version..."
|
||||
|
||||
let result = (do -i {
|
||||
plugin-secretumvault-version
|
||||
})
|
||||
|
||||
if $result != null {
|
||||
print $"✓ Version information\n"
|
||||
print $"SecretumVault Version: ($result)"
|
||||
} else {
|
||||
print $"❌ Version check failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Show plugin status and configuration
|
||||
def handle_sv_status [flags: record] {
|
||||
print $"SecretumVault Plugin Status\n"
|
||||
|
||||
let info = (do -i {
|
||||
plugin-secretumvault-info
|
||||
})
|
||||
|
||||
if $info != null {
|
||||
print $"Plugin Available: ($info.plugin_available)"
|
||||
print $"Plugin Enabled: ($info.plugin_enabled)"
|
||||
print $"Service URL: ($info.service_url)"
|
||||
print $"Mount Point: ($info.mount_point)"
|
||||
print $"Default Key: ($info.default_key)"
|
||||
print $"Authenticated: ($info.authenticated)"
|
||||
print $"Mode: ($info.mode)"
|
||||
} else {
|
||||
print $"❌ Status check failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Show SecretumVault help
|
||||
def show_sv_help [] {
|
||||
print "╔════════════════════════════════════════════════════════╗"
|
||||
print "║ 🔐 SECRETUMVAULT KMS OPERATIONS ║"
|
||||
print "╚════════════════════════════════════════════════════════╝"
|
||||
print ""
|
||||
print "DESCRIPTION"
|
||||
print " SecretumVault is a post-quantum ready KMS system"
|
||||
print " for secure encryption, decryption, and key management"
|
||||
print ""
|
||||
print "COMMANDS"
|
||||
print " encrypt <plaintext> [--key-id <key>]"
|
||||
print " Encrypt plaintext data"
|
||||
print ""
|
||||
print " decrypt <ciphertext> [--key-id <key>]"
|
||||
print " Decrypt ciphertext"
|
||||
print ""
|
||||
print " generate-key [--bits <int>] [--key-id <key>]"
|
||||
print " Generate new encryption key, 256 bits default"
|
||||
print ""
|
||||
print " encrypt-file <file> [--output <path>] [--key-id <key>]"
|
||||
print " Encrypt configuration file to .enc format"
|
||||
print ""
|
||||
print " decrypt-file <file> [--output <path>] [--key-id <key>]"
|
||||
print " Decrypt encrypted configuration file"
|
||||
print ""
|
||||
print " rotate-key [--key-id <key>]"
|
||||
print " Rotate encryption key"
|
||||
print ""
|
||||
print " health"
|
||||
print " Check SecretumVault service health"
|
||||
print ""
|
||||
print " version"
|
||||
print " Display SecretumVault version"
|
||||
print ""
|
||||
print " status"
|
||||
print " Show plugin configuration and mode"
|
||||
print ""
|
||||
print "ENVIRONMENT VARIABLES"
|
||||
print " SECRETUMVAULT_URL Service URL, http://localhost:8200 default"
|
||||
print " SECRETUMVAULT_TOKEN Authentication token, required"
|
||||
print " SECRETUMVAULT_MOUNT_POINT Vault mount path, transit default"
|
||||
print " SECRETUMVAULT_KEY_NAME Key name, provisioning-master default"
|
||||
print " SECRETUMVAULT_TLS_VERIFY Enable TLS verification, false default"
|
||||
print ""
|
||||
print "EXAMPLES"
|
||||
print " provisioning secretumvault encrypt 'my-secret' --key-id master-key"
|
||||
print " provisioning secretumvault decrypt 'vault:v1:...' --key-id master-key"
|
||||
print " provisioning secretumvault health"
|
||||
print " provisioning secretumvault version"
|
||||
print ""
|
||||
print "SHORTCUTS"
|
||||
print " sv → secretumvault, vault → secretumvault, enc → encrypt"
|
||||
print " dec → decrypt, health → check, version → ver, status → info"
|
||||
print ""
|
||||
print "For more info: docs/user/SECRETUMVAULT_KMS_GUIDE.md"
|
||||
}
|
||||
679
nulib/cli/handlers/setup.nu
Normal file
679
nulib/cli/handlers/setup.nu
Normal file
|
|
@ -0,0 +1,679 @@
|
|||
# Setup Command Handler
|
||||
# Routes setup subcommands to appropriate setup modules
|
||||
# Integrates with complete setup system
|
||||
|
||||
use platform/setup/mod.nu *
|
||||
use platform/setup/detection.nu *
|
||||
use platform/setup/validation.nu *
|
||||
use platform/setup/wizard.nu *
|
||||
use platform/setup/system.nu *
|
||||
use platform/setup/platform.nu *
|
||||
use platform/setup/provider.nu *
|
||||
use platform/setup/provctl_integration.nu *
|
||||
|
||||
# Main setup command handler
|
||||
export def cmd-setup [
|
||||
command: string
|
||||
args: list<string>
|
||||
--check = false
|
||||
--verbose = false
|
||||
--yes = false
|
||||
--interactive = false
|
||||
] {
|
||||
# Parse command and route appropriately
|
||||
match $command {
|
||||
"system" => {
|
||||
setup-command-system $args --check=$check --verbose=$verbose --interactive=$interactive
|
||||
}
|
||||
|
||||
"workspace" => {
|
||||
setup-command-workspace $args --check=$check --verbose=$verbose
|
||||
}
|
||||
|
||||
"provider" => {
|
||||
setup-command-provider $args --check=$check --verbose=$verbose
|
||||
}
|
||||
|
||||
"platform" => {
|
||||
setup-command-platform $args --check=$check --verbose=$verbose
|
||||
}
|
||||
|
||||
"profile" => {
|
||||
setup-command-profile $args --check=$check --verbose=$verbose --interactive=$interactive --yes=$yes
|
||||
}
|
||||
|
||||
"update" => {
|
||||
setup-command-update $args --check=$check --verbose=$verbose
|
||||
}
|
||||
|
||||
"wizard" | "wiz" => {
|
||||
setup-command-wizard --verbose=$verbose
|
||||
}
|
||||
|
||||
"validate" | "val" => {
|
||||
setup-command-validate --verbose=$verbose
|
||||
}
|
||||
|
||||
"detect" | "detection" => {
|
||||
setup-command-detect --verbose=$verbose
|
||||
}
|
||||
|
||||
"migrate" | "migration" => {
|
||||
setup-command-migrate $args --verbose=$verbose
|
||||
}
|
||||
|
||||
"status" => {
|
||||
setup-command-status --verbose=$verbose
|
||||
}
|
||||
|
||||
"versions" | "gen-versions" => {
|
||||
setup-command-versions $args --verbose=$verbose
|
||||
}
|
||||
|
||||
"help" | "h" | "" => {
|
||||
print-setup-help
|
||||
}
|
||||
|
||||
_ => {
|
||||
print-setup-help
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# SETUP SUBCOMMAND HANDLERS
|
||||
# ============================================================================
|
||||
|
||||
# Complete system setup
|
||||
def setup-command-system [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
--interactive
|
||||
] {
|
||||
# Only show help if explicitly requested
|
||||
if ($args | any { |a| $a == "--help" or $a == "-h" }) {
|
||||
print ""
|
||||
print "Setup System Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup system [OPTIONS]"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --interactive Run interactive wizard (default)"
|
||||
print " --defaults Use recommended defaults"
|
||||
print " --minimal Minimal configuration"
|
||||
print " --check, -c Dry-run without changes"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup system --interactive"
|
||||
print " provisioning setup system --defaults"
|
||||
print " provisioning setup system --minimal"
|
||||
return
|
||||
}
|
||||
|
||||
let mode = if ($args | any { |a| $a == "--defaults" }) {
|
||||
"defaults"
|
||||
} else if ($args | any { |a| $a == "--minimal" }) {
|
||||
"minimal"
|
||||
} else {
|
||||
"interactive"
|
||||
}
|
||||
|
||||
match $mode {
|
||||
"interactive" => {
|
||||
print ""
|
||||
let result = (run-interactive-setup --verbose=$verbose)
|
||||
if $result.success {
|
||||
print-setup-success "System setup completed successfully!"
|
||||
print-setup-status
|
||||
} else {
|
||||
print-setup-error $"Setup failed: ($result.reason)"
|
||||
}
|
||||
}
|
||||
"defaults" => {
|
||||
print ""
|
||||
let result = (run-setup-defaults --verbose=$verbose)
|
||||
if $result.success {
|
||||
print-setup-success "System setup completed with defaults!"
|
||||
print-setup-status
|
||||
} else {
|
||||
print-setup-error $"Setup failed: ($result.reason)"
|
||||
}
|
||||
}
|
||||
"minimal" => {
|
||||
print ""
|
||||
let result = (run-setup-minimal --verbose=$verbose)
|
||||
if $result.success {
|
||||
print-setup-success "Minimal system setup completed!"
|
||||
print-setup-status
|
||||
} else {
|
||||
print-setup-error $"Setup failed: ($result.reason)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Workspace setup
|
||||
def setup-command-workspace [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
] {
|
||||
if ($args | length) == 0 {
|
||||
print ""
|
||||
print "Setup Workspace"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup workspace <name> [OPTIONS]"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --check, -c Dry-run without changes"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup workspace myproject"
|
||||
print " provisioning setup workspace myproject --check"
|
||||
return
|
||||
}
|
||||
|
||||
let workspace_name = ($args | get 0)
|
||||
print-setup-header $"Setting up Workspace: ($workspace_name)"
|
||||
print ""
|
||||
print-setup-info "Workspace setup not yet implemented"
|
||||
print "Please use: provisioning workspace init ($workspace_name)"
|
||||
}
|
||||
|
||||
# Provider setup
|
||||
def setup-command-provider [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
] {
|
||||
if ($args | length) == 0 {
|
||||
print ""
|
||||
print "Setup Provider Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup provider <name> [OPTIONS]"
|
||||
print ""
|
||||
print "AVAILABLE PROVIDERS:"
|
||||
print " upcloud UpCloud infrastructure provider"
|
||||
print " aws Amazon Web Services provider"
|
||||
print " hetzner Hetzner Cloud provider"
|
||||
print " local Local development provider (no credentials needed)"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --check, -c Dry-run without changes"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup provider upcloud"
|
||||
print " provisioning setup provider aws --verbose"
|
||||
print ""
|
||||
print "CREDENTIALS:"
|
||||
print "All provider credentials are stored encrypted in RustyVault."
|
||||
print "Config files contain references only, never actual credentials."
|
||||
return
|
||||
}
|
||||
|
||||
let provider = ($args | get 0)
|
||||
let config_base = (get-config-base-path)
|
||||
let result = (setup-provider $provider $config_base)
|
||||
|
||||
if $result.success {
|
||||
print-setup-success $"Provider ($provider) configured successfully"
|
||||
print-provider-setup-instructions
|
||||
} else {
|
||||
print-setup-error $result.error
|
||||
}
|
||||
}
|
||||
|
||||
# Platform services setup
|
||||
def setup-command-platform [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
] {
|
||||
if ($args | length) == 0 {
|
||||
print ""
|
||||
print "Setup Platform Services"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup platform [MODE] [OPTIONS]"
|
||||
print ""
|
||||
print "MODES:"
|
||||
print " solo Single-user local development (Docker Compose)"
|
||||
print " multiuser Team environment (Docker/Kubernetes/SSH)"
|
||||
print " cicd CI/CD pipeline deployment (Kubernetes)"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --check, -c Dry-run without changes"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup platform solo"
|
||||
print " provisioning setup platform multiuser --verbose"
|
||||
print " provisioning setup platform cicd --check"
|
||||
return
|
||||
}
|
||||
|
||||
let mode = ($args | get 0) | default "solo"
|
||||
let result = (setup-platform-complete $mode --verbose=$verbose)
|
||||
|
||||
if $result.success {
|
||||
print-setup-success "Platform services setup completed"
|
||||
print-platform-status
|
||||
} else {
|
||||
print-setup-error $"Platform setup failed: ($result.error)"
|
||||
}
|
||||
}
|
||||
|
||||
# Unified profile-based setup
|
||||
def setup-command-profile [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
--interactive
|
||||
--yes
|
||||
] {
|
||||
if ($args | any { |a| $a == "--help" or $a == "-h" }) {
|
||||
print ""
|
||||
print "Setup via Profile (Unified Setup System)"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup profile [OPTIONS]"
|
||||
print " provisioning setup profile --profile <developer|production|cicd>"
|
||||
print ""
|
||||
print "PROFILES:"
|
||||
print " developer Fast local setup (<5 min, Docker Compose)"
|
||||
print " production Full validated setup (Kubernetes, HA, security)"
|
||||
print " cicd Ephemeral pipeline setup (automated, cleanup)"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --profile <PROFILE> Specify profile (asks if not provided)"
|
||||
print " --interactive, -i Interactive mode (default if TTY)"
|
||||
print " --yes, -y Skip confirmations"
|
||||
print " --check, -c Dry-run without changes"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup profile --profile developer"
|
||||
print " provisioning setup profile --profile production --interactive"
|
||||
print " provisioning setup profile --yes --verbose"
|
||||
return
|
||||
}
|
||||
|
||||
# Extract profile from args or prompt
|
||||
mut profile = ""
|
||||
mut idx = 0
|
||||
while ($idx < ($args | length)) {
|
||||
let arg = ($args | get $idx)
|
||||
if ($arg | str starts-with "--profile") {
|
||||
if ($arg | str contains "=") {
|
||||
# Format: --profile=developer
|
||||
let parts = ($arg | split column "=" --collapse-empty)
|
||||
$profile = (($parts.column1 | get 1) | str trim)
|
||||
} else if (($idx + 1) < ($args | length)) {
|
||||
# Format: --profile developer
|
||||
$profile = ($args | get ($idx + 1) | str trim)
|
||||
$idx = ($idx + 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
$idx = ($idx + 1)
|
||||
}
|
||||
|
||||
# Determine profile to use
|
||||
let selected_profile = if ($profile != "") {
|
||||
# Validate profile argument
|
||||
if ($profile in ["developer", "production", "cicd"]) {
|
||||
$profile
|
||||
} else {
|
||||
print-setup-error $"Invalid profile: ($profile). Must be: developer, production, or cicd"
|
||||
return
|
||||
}
|
||||
} else if $interactive {
|
||||
# Interactive mode - prompt for profile
|
||||
(prompt-profile-selection)
|
||||
} else if $yes {
|
||||
# Assume developer profile if --yes without --profile
|
||||
"developer"
|
||||
} else {
|
||||
# Default to interactive mode
|
||||
(prompt-profile-selection)
|
||||
}
|
||||
|
||||
print-setup-header "Setup Profile: $(($selected_profile | str upcase))"
|
||||
print ""
|
||||
|
||||
# Get config base path (platform-specific)
|
||||
let config_base = (get-config-base-path)
|
||||
|
||||
if $check {
|
||||
print-setup-warning "DRY-RUN MODE - No changes will be made"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Execute profile-specific setup
|
||||
let result = (match $selected_profile {
|
||||
"developer" => {
|
||||
setup-platform-developer $config_base --verbose=$verbose
|
||||
}
|
||||
"production" => {
|
||||
setup-platform-production $config_base --verbose=$verbose
|
||||
}
|
||||
"cicd" => {
|
||||
setup-platform-cicd-nickel $config_base --verbose=$verbose
|
||||
}
|
||||
_ => {
|
||||
{
|
||||
success: false
|
||||
error: $"Unknown profile: ($selected_profile)"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if $result.success {
|
||||
print-setup-success $"Profile setup completed: ($selected_profile)"
|
||||
print ""
|
||||
print "Configuration Details:"
|
||||
print $" Profile: ($selected_profile)"
|
||||
print $" Location: ($config_base)"
|
||||
print $" Deployment: ($result.deployment | default 'unknown')"
|
||||
print ""
|
||||
print-setup-success "Services configured and ready to start"
|
||||
} else {
|
||||
print-setup-error $"Profile setup failed: ($result.error)"
|
||||
}
|
||||
}
|
||||
|
||||
# Update configuration
|
||||
def setup-command-update [
|
||||
args: list<string>
|
||||
--check
|
||||
--verbose
|
||||
] {
|
||||
print ""
|
||||
print "Update Provisioning Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup update [COMPONENT] [OPTIONS]"
|
||||
print ""
|
||||
print "COMPONENTS:"
|
||||
print " provider Update provider configuration"
|
||||
print " platform Update platform services"
|
||||
print " preferences Update user preferences"
|
||||
print ""
|
||||
print "This command updates existing configuration without re-running"
|
||||
print "the complete setup wizard. IMMUTABLE settings cannot be changed."
|
||||
print ""
|
||||
print-setup-info "Update functionality not yet implemented"
|
||||
}
|
||||
|
||||
# Interactive wizard
|
||||
def setup-command-wizard [
|
||||
--verbose
|
||||
] {
|
||||
let result = (run-setup-wizard --verbose=$verbose)
|
||||
|
||||
if $result.completed {
|
||||
print ""
|
||||
print-setup-success "Setup configuration confirmed!"
|
||||
print ""
|
||||
} else {
|
||||
print ""
|
||||
print-setup-warning "Setup wizard cancelled by user"
|
||||
}
|
||||
}
|
||||
|
||||
# Validate configuration
|
||||
def setup-command-validate [
|
||||
--verbose
|
||||
] {
|
||||
print-setup-header "Validating Provisioning Configuration"
|
||||
print ""
|
||||
|
||||
let config_base = (get-config-base-path)
|
||||
|
||||
# Load and validate system config
|
||||
let system_config = (load-config-toml $"($config_base)/system.toml")
|
||||
let system_validation = (validate-system-config $system_config)
|
||||
|
||||
if $verbose {
|
||||
print-validation-report $system_validation
|
||||
}
|
||||
|
||||
# Load and validate platform config
|
||||
let platform_config = (load-config-toml $"($config_base)/platform/deployment.toml")
|
||||
let platform_validation = (validate-platform-config $platform_config)
|
||||
|
||||
if $verbose {
|
||||
print-validation-report $platform_validation
|
||||
}
|
||||
|
||||
if ($system_validation.valid and $platform_validation.valid) {
|
||||
print-setup-success "All configurations are valid!"
|
||||
} else {
|
||||
print-setup-error "Some configurations have errors"
|
||||
}
|
||||
}
|
||||
|
||||
# Detect system capabilities
|
||||
def setup-command-detect [
|
||||
--verbose
|
||||
] {
|
||||
print-setup-header "Detecting System Capabilities"
|
||||
print ""
|
||||
|
||||
let detection_report = (generate-detection-report)
|
||||
print-detection-report $detection_report
|
||||
|
||||
if $verbose {
|
||||
print ""
|
||||
print "Recommended Settings:"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
let recommended = (get-recommended-config $detection_report)
|
||||
print $"Deployment Mode: ($recommended.deployment_mode)"
|
||||
print $"Supported Modes: ($recommended.supported_modes | to json)"
|
||||
if ($recommended.required_tools_missing | length) > 0 {
|
||||
print ""
|
||||
print "⚠️ Missing Required Tools:"
|
||||
for tool in $recommended.required_tools_missing {
|
||||
print $" • ($tool)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Migration
|
||||
def setup-command-migrate [
|
||||
args: list<string>
|
||||
--verbose
|
||||
] {
|
||||
if ($args | length) == 0 {
|
||||
print ""
|
||||
print "Migrate Existing Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "This command migrates existing workspace configurations to the"
|
||||
print "new setup system with automatic backup and rollback support."
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup migrate [OPTIONS]"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --auto Automatically migrate detected workspaces"
|
||||
print " --workspace <name> Migrate specific workspace"
|
||||
print " --rollback Rollback a migration"
|
||||
print " --verbose, -v Verbose output"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup migrate --auto"
|
||||
print " provisioning setup migrate --workspace librecloud"
|
||||
print " provisioning setup migrate --rollback --workspace librecloud"
|
||||
return
|
||||
}
|
||||
|
||||
let config_base = (get-config-base-path)
|
||||
|
||||
let auto_migrate = ($args | any { |a| $a == "--auto" })
|
||||
if $auto_migrate {
|
||||
let result = (auto-migrate-existing $config_base --verbose=$verbose)
|
||||
if $result.success {
|
||||
print-setup-success $"Migration completed: ($result.migrated_count) workspace(s) migrated"
|
||||
} else {
|
||||
print-setup-error "Migration failed"
|
||||
}
|
||||
} else {
|
||||
print-setup-info "No migration options specified"
|
||||
}
|
||||
}
|
||||
|
||||
# Setup status
|
||||
def setup-command-status [
|
||||
--verbose
|
||||
] {
|
||||
print-setup-status
|
||||
print-platform-status
|
||||
|
||||
if $verbose {
|
||||
print ""
|
||||
print "Detailed Information:"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
let detection = (generate-detection-report)
|
||||
print-detection-report $detection
|
||||
}
|
||||
}
|
||||
|
||||
# Generate versions file from versions.ncl
|
||||
def setup-command-versions [
|
||||
args: list<string>
|
||||
--verbose
|
||||
] {
|
||||
use platform/setup/utils.nu [create_versions_file]
|
||||
|
||||
if ($args | any { |a| $a == "--help" or $a == "-h" }) {
|
||||
print ""
|
||||
print "Generate Versions File"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup versions [OPTIONS]"
|
||||
print ""
|
||||
print "OPTIONS:"
|
||||
print " --output FILE Output filename (default: versions)"
|
||||
print " --help, -h Show this help message"
|
||||
print ""
|
||||
print "DESCRIPTION:"
|
||||
print " Generates a bash-compatible versions file from versions.ncl"
|
||||
print " in KEY=VALUE format for use by shell scripts."
|
||||
print ""
|
||||
return
|
||||
}
|
||||
|
||||
let output_file = (
|
||||
if ($args | any { |a| $a == "--output" }) {
|
||||
let idx = ($args | position { |a| $a == "--output" })
|
||||
if ($idx + 1) < ($args | length) {
|
||||
$args | get ($idx + 1)
|
||||
} else {
|
||||
"versions"
|
||||
}
|
||||
} else {
|
||||
"versions"
|
||||
}
|
||||
)
|
||||
|
||||
print-setup-info $"Generating versions file: ($output_file)"
|
||||
|
||||
if (create_versions_file $output_file) {
|
||||
print-setup-success $"Versions file generated successfully"
|
||||
if $verbose {
|
||||
let provisioning = ($env.PROVISIONING? | default ($env.PWD))
|
||||
let versions_file = ($provisioning | path join "core" | path join $output_file)
|
||||
print $"Location: ($versions_file)"
|
||||
print ""
|
||||
print "Content:"
|
||||
print (open $versions_file)
|
||||
}
|
||||
} else {
|
||||
print-setup-error "Failed to generate versions file"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# HELP DISPLAY
|
||||
# ============================================================================
|
||||
|
||||
def print-setup-help [] {
|
||||
print ""
|
||||
print "╔═══════════════════════════════════════════════════════════════╗"
|
||||
print "║ PROVISIONING SETUP SYSTEM ║"
|
||||
print "║ ║"
|
||||
print "║ Complete system initialization and configuration management ║"
|
||||
print "╚═══════════════════════════════════════════════════════════════╝"
|
||||
print ""
|
||||
|
||||
print "USAGE:"
|
||||
print " provisioning setup <command> [OPTIONS]"
|
||||
print ""
|
||||
|
||||
print "COMMANDS:"
|
||||
print " system Complete system setup (interactive, defaults, minimal)"
|
||||
print " workspace Create and configure workspaces"
|
||||
print " provider Setup infrastructure providers (UpCloud, AWS, Hetzner)"
|
||||
print " platform Configure platform services (Orchestrator, Control-Center, KMS)"
|
||||
print " update Update existing configuration"
|
||||
print " wizard Interactive setup wizard"
|
||||
print " validate Validate current configuration"
|
||||
print " detect Detect system capabilities"
|
||||
print " migrate Migrate existing configurations"
|
||||
print " versions Generate bash-compatible versions file"
|
||||
print " status Show setup status"
|
||||
print " help Show this help message"
|
||||
print ""
|
||||
|
||||
print "COMMON EXAMPLES:"
|
||||
print " # Interactive setup (recommended)"
|
||||
print " provisioning setup system --interactive"
|
||||
print ""
|
||||
print " # Setup with defaults"
|
||||
print " provisioning setup system --defaults"
|
||||
print ""
|
||||
print " # Check system capabilities"
|
||||
print " provisioning setup detect --verbose"
|
||||
print ""
|
||||
print " # Validate configuration"
|
||||
print " provisioning setup validate"
|
||||
print ""
|
||||
print " # Migrate existing workspace"
|
||||
print " provisioning setup migrate --auto"
|
||||
print ""
|
||||
print " # Generate bash versions file (for shell scripts)"
|
||||
print " provisioning setup versions"
|
||||
print ""
|
||||
|
||||
print "OPTIONS:"
|
||||
print " --check, -c Dry-run without making changes"
|
||||
print " --verbose, -v Show detailed output"
|
||||
print " --yes, -y Auto-confirm prompts"
|
||||
print ""
|
||||
|
||||
print "CONFIGURATION LOCATIONS:"
|
||||
print " macOS: ~/Library/Application Support/provisioning/"
|
||||
print " Linux: ~/.config/provisioning/"
|
||||
print " Windows: %APPDATA%/provisioning/"
|
||||
print ""
|
||||
|
||||
print "For help on specific commands:"
|
||||
print " provisioning setup <command> --help"
|
||||
print ""
|
||||
}
|
||||
219
nulib/cli/handlers/setup_simple.nu
Normal file
219
nulib/cli/handlers/setup_simple.nu
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# Simplified Setup Command Handler
|
||||
# Provides essential setup functionality without complex dependencies
|
||||
|
||||
# Main setup command handler
|
||||
export def cmd-setup-simple [
|
||||
command: string
|
||||
...args: string
|
||||
--check
|
||||
--verbose
|
||||
--yes
|
||||
--interactive
|
||||
] {
|
||||
# Parse command and route appropriately
|
||||
match $command {
|
||||
"system" => {
|
||||
print ""
|
||||
print "╔═══════════════════════════════════════════════════════════════╗"
|
||||
print "║ PROVISIONING SETUP SYSTEM ║"
|
||||
print "║ ║"
|
||||
print "║ Complete system initialization and configuration management ║"
|
||||
print "╚═══════════════════════════════════════════════════════════════╝"
|
||||
print ""
|
||||
print "System setup modes:"
|
||||
print ""
|
||||
print " --interactive Run interactive wizard (default)"
|
||||
print " --defaults Use recommended defaults"
|
||||
print " --minimal Minimal configuration"
|
||||
print ""
|
||||
print "To proceed with system setup:"
|
||||
print " provisioning setup system --interactive"
|
||||
print ""
|
||||
}
|
||||
|
||||
"workspace" => {
|
||||
print ""
|
||||
print "Setup Workspace"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup workspace <name> [OPTIONS]"
|
||||
print ""
|
||||
print "EXAMPLES:"
|
||||
print " provisioning setup workspace myproject"
|
||||
print " provisioning setup workspace myproject --check"
|
||||
print ""
|
||||
}
|
||||
|
||||
"provider" => {
|
||||
print ""
|
||||
print "Setup Provider Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "AVAILABLE PROVIDERS:"
|
||||
print " upcloud UpCloud infrastructure provider"
|
||||
print " aws Amazon Web Services provider"
|
||||
print " hetzner Hetzner Cloud provider"
|
||||
print " local Local development provider (no credentials needed)"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup provider <name> [OPTIONS]"
|
||||
print ""
|
||||
print "All provider credentials are stored encrypted in RustyVault."
|
||||
print "Config files contain references only, never actual credentials."
|
||||
print ""
|
||||
}
|
||||
|
||||
"platform" => {
|
||||
print ""
|
||||
print "Setup Platform Services"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "MODES:"
|
||||
print " solo Single-user local development (Docker Compose)"
|
||||
print " multiuser Team environment (Docker/Kubernetes/SSH)"
|
||||
print " cicd CI/CD pipeline deployment (Kubernetes)"
|
||||
print ""
|
||||
print "USAGE:"
|
||||
print " provisioning setup platform [MODE] [OPTIONS]"
|
||||
print ""
|
||||
}
|
||||
|
||||
"validate" | "val" => {
|
||||
print ""
|
||||
print "Validating Provisioning Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "✅ Configuration validation"
|
||||
print ""
|
||||
}
|
||||
|
||||
"detect" | "detection" => {
|
||||
print ""
|
||||
print "Detecting System Capabilities"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
|
||||
# Detect OS
|
||||
let os = (
|
||||
match $nu.os-info.name {
|
||||
"linux" => "Linux"
|
||||
"macos" => "macOS"
|
||||
"windows" => "Windows"
|
||||
_ => $nu.os-info.name
|
||||
}
|
||||
)
|
||||
print $"✅ OS: ($os)"
|
||||
|
||||
# Detect architecture
|
||||
let arch = (
|
||||
match $nu.os-info.arch {
|
||||
"x86_64" => "x86_64 (Intel/AMD)"
|
||||
"aarch64" => "aarch64 (ARM)"
|
||||
_ => $nu.os-info.arch
|
||||
}
|
||||
)
|
||||
print $"✅ Architecture: ($arch)"
|
||||
|
||||
# Check deployment tools
|
||||
if (which docker | is-not-empty) {
|
||||
print "✅ Docker is available"
|
||||
} else {
|
||||
print "⚠️ Docker not found"
|
||||
}
|
||||
|
||||
if (which kubectl | is-not-empty) {
|
||||
print "✅ Kubernetes (kubectl) is available"
|
||||
} else {
|
||||
print "⚠️ Kubernetes not found"
|
||||
}
|
||||
|
||||
if (which ssh | is-not-empty) {
|
||||
print "✅ SSH is available"
|
||||
} else {
|
||||
print "⚠️ SSH not found"
|
||||
}
|
||||
|
||||
print ""
|
||||
}
|
||||
|
||||
"migrate" | "migration" => {
|
||||
print ""
|
||||
print "Migrate Existing Configuration"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "This command migrates existing workspace configurations to the"
|
||||
print "new setup system with automatic backup and rollback support."
|
||||
print ""
|
||||
}
|
||||
|
||||
"status" => {
|
||||
print ""
|
||||
print "Setup Status"
|
||||
print "─────────────────────────────────────────────────────────────"
|
||||
print ""
|
||||
print "System configuration status: OK"
|
||||
print "Platform services: Configured"
|
||||
print ""
|
||||
}
|
||||
|
||||
"help" | "h" | "" => {
|
||||
print-setup-help
|
||||
}
|
||||
|
||||
_ => {
|
||||
print-setup-help
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def print-setup-help [] {
|
||||
print ""
|
||||
print "╔═══════════════════════════════════════════════════════════════╗"
|
||||
print "║ PROVISIONING SETUP SYSTEM ║"
|
||||
print "║ ║"
|
||||
print "║ Complete system initialization and configuration management ║"
|
||||
print "╚═══════════════════════════════════════════════════════════════╝"
|
||||
print ""
|
||||
|
||||
print "USAGE:"
|
||||
print " provisioning setup <command> [OPTIONS]"
|
||||
print ""
|
||||
|
||||
print "COMMANDS:"
|
||||
print " system Complete system setup (interactive, defaults, minimal)"
|
||||
print " workspace Create and configure workspaces"
|
||||
print " provider Setup infrastructure providers (UpCloud, AWS, Hetzner)"
|
||||
print " platform Configure platform services (Orchestrator, Control-Center, KMS)"
|
||||
print " validate Validate current configuration"
|
||||
print " detect Detect system capabilities"
|
||||
print " migrate Migrate existing configurations"
|
||||
print " status Show setup status"
|
||||
print " help Show this help message"
|
||||
print ""
|
||||
|
||||
print "COMMON EXAMPLES:"
|
||||
print " # Interactive setup (recommended)"
|
||||
print " provisioning setup system --interactive"
|
||||
print ""
|
||||
print " # Setup with defaults"
|
||||
print " provisioning setup system --defaults"
|
||||
print ""
|
||||
print " # Check system capabilities"
|
||||
print " provisioning setup detect"
|
||||
print ""
|
||||
print " # Validate configuration"
|
||||
print " provisioning setup validate"
|
||||
print ""
|
||||
|
||||
print "OPTIONS:"
|
||||
print " --check, -c Dry-run without making changes"
|
||||
print " --verbose, -v Show detailed output"
|
||||
print " --yes, -y Auto-confirm prompts"
|
||||
print " --interactive Use interactive mode"
|
||||
print ""
|
||||
|
||||
print "For help on specific commands:"
|
||||
print " provisioning setup <command> --help"
|
||||
print ""
|
||||
}
|
||||
127
nulib/cli/handlers/state.nu
Normal file
127
nulib/cli/handlers/state.nu
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use platform/config/accessor.nu *
|
||||
use primitives/io/interface.nu [_print]
|
||||
use domain/workspace/state.nu *
|
||||
use domain/workspace/sync.nu *
|
||||
|
||||
export def handle_state_command [cmd: string, ops: string, flags: record] {
|
||||
let workspace_path = if ($env.PROVISIONING_WORKSPACE_PATH? | is-not-empty) {
|
||||
$env.PROVISIONING_WORKSPACE_PATH
|
||||
} else {
|
||||
$env.PWD
|
||||
}
|
||||
|
||||
let infra = ($flags | get -o infra | default "")
|
||||
let server = ($flags | get -o server | default "")
|
||||
let taskserv = ($flags | get -o taskserv | default "")
|
||||
let kubeconfig = ($flags | get -o kubeconfig | default "")
|
||||
|
||||
# When help_category == command name ("state"), the subcommand lands in $ops, not $cmd.
|
||||
let subcmd = if ($ops | is-not-empty) { ($ops | split row " " | first) } else { $cmd }
|
||||
|
||||
match $subcmd {
|
||||
"show" | "s" => {
|
||||
state-show $workspace_path --server $server
|
||||
},
|
||||
|
||||
"init" | "i" => {
|
||||
let curr_settings = (find_get_settings --infra $infra)
|
||||
state-init $workspace_path $curr_settings
|
||||
_print $"State initialized at (state-path $workspace_path)"
|
||||
},
|
||||
|
||||
"reset" | "r" => {
|
||||
if ($server | is-empty) or ($taskserv | is-empty) {
|
||||
error make { msg: "state reset requires --server <hostname> --taskserv <name>" }
|
||||
}
|
||||
state-node-reset $workspace_path $server $taskserv
|
||||
_print $"($server)/($taskserv) reset to pending"
|
||||
},
|
||||
|
||||
"migrate" | "m" => {
|
||||
state-migrate-from-json $workspace_path
|
||||
},
|
||||
|
||||
"sync" => {
|
||||
let curr_settings = (find_get_settings --infra $infra)
|
||||
|
||||
# 1. Drift detection + reconcile against servers.ncl
|
||||
let drift_rows = (state-drift $workspace_path $curr_settings --server $server)
|
||||
let has_drift = ($drift_rows | where drift != "ok" | is-not-empty)
|
||||
if $has_drift {
|
||||
_print "── drift ──"
|
||||
print ($drift_rows | where drift != "ok" | table)
|
||||
let result = (state-reconcile $workspace_path $curr_settings --server $server)
|
||||
if ($result.removed | is-not-empty) {
|
||||
_print $"🗑 Removed ($result.removed | length) orphaned"
|
||||
}
|
||||
if ($result.added | is-not-empty) {
|
||||
_print $"➕ Added ($result.added | length) pending"
|
||||
}
|
||||
} else {
|
||||
_print "✅ No drift against servers.ncl"
|
||||
}
|
||||
|
||||
# 2. External API sync (Hetzner, K8s, SSH)
|
||||
let skip_ssh = ($flags | get -o skip_ssh | default false)
|
||||
state-sync $workspace_path $curr_settings --kubeconfig $kubeconfig --skip-ssh=$skip_ssh
|
||||
},
|
||||
|
||||
"drift" | "d" => {
|
||||
let curr_settings = (find_get_settings --infra $infra)
|
||||
let rows = (state-drift $workspace_path $curr_settings --server $server)
|
||||
let has_drift = ($rows | where drift != "ok" | is-not-empty)
|
||||
if ($rows | is-empty) {
|
||||
_print "(no state entries to compare)"
|
||||
} else {
|
||||
print ($rows | table)
|
||||
if $has_drift {
|
||||
_print $"\n⚠ Drift detected. Run (_ansi yellow_bold)provisioning state reconcile(_ansi reset) to fix."
|
||||
} else {
|
||||
_print "\n✅ No drift — state matches servers.ncl"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"reconcile" | "rec" => {
|
||||
let curr_settings = (find_get_settings --infra $infra)
|
||||
let dry_run = ($flags | get -o dry_run | default false)
|
||||
|
||||
# Always show drift first
|
||||
let drift_rows = (state-drift $workspace_path $curr_settings --server $server)
|
||||
let has_drift = ($drift_rows | where drift != "ok" | is-not-empty)
|
||||
if not $has_drift {
|
||||
_print "✅ No drift — nothing to reconcile"
|
||||
return
|
||||
}
|
||||
print ($drift_rows | where drift != "ok" | table)
|
||||
|
||||
if $dry_run {
|
||||
_print "\n(dry-run: no changes applied)"
|
||||
return
|
||||
}
|
||||
|
||||
let result = (state-reconcile $workspace_path $curr_settings --server $server)
|
||||
if ($result.removed | is-not-empty) {
|
||||
_print $"\n🗑 Removed ($result.removed | length) orphaned entries:"
|
||||
for r in $result.removed { _print $" ($r.server)/($r.taskserv)" }
|
||||
}
|
||||
if ($result.added | is-not-empty) {
|
||||
_print $"\n➕ Added ($result.added | length) pending entries:"
|
||||
for a in $result.added { _print $" ($a.server)/($a.taskserv)" }
|
||||
}
|
||||
_print "\n✅ State reconciled with servers.ncl"
|
||||
},
|
||||
|
||||
_ => {
|
||||
_print "Usage: provisioning state <subcommand> [--infra <path>]"
|
||||
_print ""
|
||||
_print " show [--server <hostname>] — display state table"
|
||||
_print " init [--infra <path>] — bootstrap state from settings"
|
||||
_print " reset --server <hostname> --taskserv <name> — reset node to pending"
|
||||
_print " migrate — migrate .json → .ncl"
|
||||
_print " sync [--infra <path>] [--kubeconfig <path>] — reconcile from APIs"
|
||||
_print " drift [--infra <path>] [--server <hostname>] — detect state vs servers.ncl divergence"
|
||||
_print " reconcile [--infra <path>] [--server <hostname>] — fix drift (remove orphaned, add missing)"
|
||||
},
|
||||
}
|
||||
}
|
||||
5
nulib/cli/handlers/utilities.nu
Normal file
5
nulib/cli/handlers/utilities.nu
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Utilities Command Orchestrator
|
||||
# Re-exports utility command dispatcher and handlers
|
||||
|
||||
# Main utility dispatcher
|
||||
export use ./utilities_core.nu *
|
||||
94
nulib/cli/handlers/utilities/alias.nu
Normal file
94
nulib/cli/handlers/utilities/alias.nu
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env nu
|
||||
# alias.nu — Command alias reference
|
||||
#
|
||||
# prvng alias / prvng a / prvng al — show the full shortcut table.
|
||||
# Reads the JSON cache (~/.cache/provisioning/commands-registry.json) — no nickel export.
|
||||
|
||||
# Load commands from the JSON cache written by _validate_command in the bash wrapper.
|
||||
# Cache is at ~/.cache/provisioning/commands-registry.json, rebuilt on registry mtime change.
|
||||
# Falls back to static table if cache is absent.
|
||||
def _load-registry []: nothing -> list<record> {
|
||||
let cache = ($env.HOME | path join ".cache" | path join "provisioning" | path join "commands-registry.json")
|
||||
if not ($cache | path exists) { return [] }
|
||||
let result = (do { open --raw $cache | from json } | complete)
|
||||
if $result.exit_code != 0 { return [] }
|
||||
$result.stdout | get -o commands | default []
|
||||
}
|
||||
|
||||
# Print section header + rows for one category.
|
||||
def _print-section [title: string, rows: list<record>]: nothing -> nothing {
|
||||
if ($rows | is-empty) { return }
|
||||
print $title
|
||||
for r in $rows {
|
||||
let al = ($r.aliases | str join " " | fill -w 14 -a l)
|
||||
let cmd = $r.command
|
||||
print $" ($al) → ($cmd)"
|
||||
}
|
||||
print ""
|
||||
}
|
||||
|
||||
# Main alias list — reads registry and renders grouped alias table.
|
||||
export def alias-list []: nothing -> nothing {
|
||||
let cmds = (_load-registry)
|
||||
|
||||
if ($cmds | is-empty) {
|
||||
_alias-list-static
|
||||
return
|
||||
}
|
||||
|
||||
let rows = ($cmds
|
||||
| where {|c| ($c | get -o aliases | default []) | is-not-empty }
|
||||
| each {|c| {
|
||||
command: $c.command
|
||||
aliases: ($c | get -o aliases | default [])
|
||||
category: ($c | get -o help_category | default "other")
|
||||
}}
|
||||
| sort-by command
|
||||
)
|
||||
|
||||
print ""
|
||||
print "ALIASES"
|
||||
print "════════════════════════════════════════════════════"
|
||||
|
||||
_print-section "" ($rows | where category == "infrastructure")
|
||||
_print-section "ORCHESTRATION" ($rows | where category == "orchestration")
|
||||
|
||||
let rest = ($rows | where {|r| $r.category not-in ["infrastructure", "orchestration"] })
|
||||
if ($rest | is-not-empty) {
|
||||
_print-section "OTHER" $rest
|
||||
}
|
||||
|
||||
print "════════════════════════════════════════════════════"
|
||||
print "Tip: prvng <alias> help → subcommand details"
|
||||
print ""
|
||||
}
|
||||
|
||||
# Static fallback when registry unavailable at runtime.
|
||||
def _alias-list-static []: nothing -> nothing {
|
||||
print ""
|
||||
print "ALIASES"
|
||||
print "════════════════════════════════════════════════════"
|
||||
print ""
|
||||
print "INFRASTRUCTURE"
|
||||
print " s → server"
|
||||
print " t task → taskserv"
|
||||
print " c e comp ext → component"
|
||||
print ""
|
||||
print "ORCHESTRATION"
|
||||
print " w wflow → workflow"
|
||||
print " j → job"
|
||||
print " b bat → batch"
|
||||
print " o orch → orchestrator"
|
||||
print ""
|
||||
print "OTHER"
|
||||
print " a al → alias"
|
||||
print " ws → workspace"
|
||||
print " h → help"
|
||||
print " p plat → platform"
|
||||
print " bd → build"
|
||||
print " val → validate"
|
||||
print ""
|
||||
print "════════════════════════════════════════════════════"
|
||||
print "Tip: prvng <alias> help → subcommand details"
|
||||
print ""
|
||||
}
|
||||
184
nulib/cli/handlers/utilities/cache.nu
Normal file
184
nulib/cli/handlers/utilities/cache.nu
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# Cache Command Handler
|
||||
# Domain: Configuration and state cache management
|
||||
|
||||
# Cache command handler - Manage configuration caches
|
||||
export def handle_cache [ops: string, flags: record] {
|
||||
use domain/cache/simple_cache.nu *
|
||||
|
||||
# Parse cache subcommand
|
||||
let parts = if ($ops | is-not-empty) {
|
||||
($ops | str trim | split row " " | where { |x| ($x | is-not-empty) })
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
let subcommand = if ($parts | length) > 0 { $parts | get 0 } else { "status" }
|
||||
let args = if ($parts | length) > 1 { $parts | skip 1 } else { [] }
|
||||
|
||||
# Handle cache commands
|
||||
match $subcommand {
|
||||
"status" => {
|
||||
print ""
|
||||
cache-status
|
||||
print ""
|
||||
}
|
||||
|
||||
"config" => {
|
||||
let config_cmd = if ($args | length) > 0 { $args | get 0 } else { "show" }
|
||||
match $config_cmd {
|
||||
"show" => {
|
||||
print ""
|
||||
let config = (get-cache-config)
|
||||
let cache_base = (($env.HOME? | default "~" | path expand) | path join ".provisioning" "cache" "config")
|
||||
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
print "📋 Cache Configuration"
|
||||
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
print ""
|
||||
|
||||
print "▸ Core Settings:"
|
||||
let enabled = ($config | get --optional enabled | default true)
|
||||
print (" Enabled: " + ($enabled | into string))
|
||||
print ""
|
||||
|
||||
print "▸ Cache Location:"
|
||||
print (" Base Path: " + $cache_base)
|
||||
print ""
|
||||
|
||||
print "▸ Time-To-Live (TTL) Settings:"
|
||||
let ttl_final = ($config | get --optional ttl_final_config | default "300")
|
||||
let ttl_nickel = ($config | get --optional ttl_nickel | default "1800")
|
||||
let ttl_sops = ($config | get --optional ttl_sops | default "900")
|
||||
print (" Final Config: " + ($ttl_final | into string) + "s (5 minutes)")
|
||||
print (" Nickel Compilation: " + ($ttl_nickel | into string) + "s (30 minutes)")
|
||||
print (" SOPS Decryption: " + ($ttl_sops | into string) + "s (15 minutes)")
|
||||
print " Provider Config: 600s (10 minutes)"
|
||||
print " Platform Config: 600s (10 minutes)"
|
||||
print ""
|
||||
|
||||
print "▸ Security Settings:"
|
||||
print " SOPS File Permissions: 0600 (owner read-only)"
|
||||
print " SOPS Directory Permissions: 0700 (owner access only)"
|
||||
print ""
|
||||
|
||||
print "▸ Validation Settings:"
|
||||
print " Strict mtime Checking: true (validates all source files)"
|
||||
print ""
|
||||
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
print ""
|
||||
}
|
||||
"get" => {
|
||||
if ($args | length) > 1 {
|
||||
let setting = $args | get 1
|
||||
let value = (cache-config-get $setting)
|
||||
if $value != null {
|
||||
print $"($setting) = ($value)"
|
||||
} else {
|
||||
print $"Setting not found: ($setting)"
|
||||
}
|
||||
} else {
|
||||
print "❌ cache config get requires a setting path"
|
||||
print "Usage: provisioning cache config get <path>"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if ($args | length) > 2 {
|
||||
let setting = $args | get 1
|
||||
let value = ($args | skip 2 | str join " ")
|
||||
cache-config-set $setting $value
|
||||
print $"✓ Set ($setting) = ($value)"
|
||||
} else {
|
||||
print "❌ cache config set requires setting path and value"
|
||||
print "Usage: provisioning cache config set <path> <value>"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
print $"❌ Unknown cache config subcommand: ($config_cmd)"
|
||||
print ""
|
||||
print "Available cache config subcommands:"
|
||||
print " show - Show all cache configuration"
|
||||
print " get <setting> - Get specific cache setting"
|
||||
print " set <key> <val> - Set cache setting"
|
||||
print ""
|
||||
print "Available settings for get/set:"
|
||||
print " enabled - Cache enabled (true/false)"
|
||||
print " ttl_final_config - TTL for final config (seconds)"
|
||||
print " ttl_nickel - TTL for Nickel compilation (seconds)"
|
||||
print " ttl_sops - TTL for SOPS decryption (seconds)"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning cache config show"
|
||||
print " provisioning cache config get ttl_final_config"
|
||||
print " provisioning cache config set ttl_final_config 600"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"clear" => {
|
||||
let cache_type = if ($args | length) > 0 { $args | get 0 } else { "all" }
|
||||
cache-clear $cache_type
|
||||
print $"✓ Cleared cache: ($cache_type)"
|
||||
}
|
||||
|
||||
"list" => {
|
||||
let cache_type = if ($args | length) > 0 { $args | get 0 } else { "*" }
|
||||
let items = (cache-list $cache_type)
|
||||
if ($items | length) > 0 {
|
||||
print $"Cache items \(type: ($cache_type)\):"
|
||||
$items | each { |item| print $" ($item)" }
|
||||
} else {
|
||||
print "No cache items found"
|
||||
}
|
||||
}
|
||||
|
||||
"help" => {
|
||||
print "
|
||||
Cache Management Commands:
|
||||
|
||||
provisioning cache status # Show cache status and statistics
|
||||
provisioning cache config show # Show cache configuration
|
||||
provisioning cache config get <setting> # Get specific cache setting
|
||||
provisioning cache config set <setting> <val> # Set cache setting
|
||||
provisioning cache clear [type] # Clear cache (default: all)
|
||||
provisioning cache list [type] # List cached items (default: all)
|
||||
provisioning cache help # Show this help message
|
||||
|
||||
Available settings (for get/set):
|
||||
enabled - Cache enabled (true/false)
|
||||
ttl_final_config - TTL for final config (seconds)
|
||||
ttl_nickel - TTL for Nickel compilation (seconds)
|
||||
ttl_sops - TTL for SOPS decryption (seconds)
|
||||
|
||||
Examples:
|
||||
provisioning cache status
|
||||
provisioning cache config get ttl_final_config
|
||||
provisioning cache config set ttl_final_config 600
|
||||
provisioning cache config set enabled false
|
||||
provisioning cache clear nickel
|
||||
provisioning cache list
|
||||
"
|
||||
}
|
||||
|
||||
_ => {
|
||||
print $"❌ Unknown cache command: ($subcommand)"
|
||||
print ""
|
||||
print "Available cache commands:"
|
||||
print " status - Show cache status and statistics"
|
||||
print " config show - Show cache configuration"
|
||||
print " config get <key> - Get specific cache setting"
|
||||
print " config set <k> <v> - Set cache setting"
|
||||
print " clear [type] - Clear cache (all, nickel, sops, final)"
|
||||
print " list [type] - List cached items"
|
||||
print " help - Show this help message"
|
||||
print ""
|
||||
print "Examples:"
|
||||
print " provisioning cache status"
|
||||
print " provisioning cache config get ttl_final_config"
|
||||
print " provisioning cache config set ttl_final_config 600"
|
||||
print " provisioning cache clear nickel"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
127
nulib/cli/handlers/utilities/guides.nu
Normal file
127
nulib/cli/handlers/utilities/guides.nu
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Guide Command Handlers
|
||||
# Domain: Interactive guide system for step-by-step instructions
|
||||
|
||||
# Guide command handler - Show interactive guides
|
||||
export def handle_guide [ops: string, flags: record] {
|
||||
let guide_topic = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
# Define guide topics and their paths
|
||||
let guides = {
|
||||
"quickstart": "docs/guides/quickstart-cheatsheet.md",
|
||||
"from-scratch": "docs/guides/from-scratch.md",
|
||||
"scratch": "docs/guides/from-scratch.md",
|
||||
"start": "docs/guides/from-scratch.md",
|
||||
"deploy": "docs/guides/from-scratch.md",
|
||||
"list": "list_guides"
|
||||
}
|
||||
|
||||
# Get docs directory
|
||||
let docs_dir = ($env.PROVISIONING_PATH | path join "docs" "guides")
|
||||
|
||||
match $guide_topic {
|
||||
"" => {
|
||||
# Show guide list
|
||||
show_guide_list $docs_dir
|
||||
}
|
||||
|
||||
"list" => {
|
||||
show_guide_list $docs_dir
|
||||
}
|
||||
|
||||
_ => {
|
||||
# Try to find and display guide
|
||||
let guide_path = if ($guide_topic in ($guides | columns)) { $guides | get $guide_topic } else { null }
|
||||
|
||||
if ($guide_path == null or $guide_path == "list_guides") {
|
||||
print $"(_ansi red)❌ Unknown guide:(_ansi reset) ($guide_topic)"
|
||||
print ""
|
||||
show_guide_list $docs_dir
|
||||
exit 1
|
||||
}
|
||||
|
||||
let full_path = ($env.PROVISIONING_PATH | path join $guide_path)
|
||||
|
||||
if not ($full_path | path exists) {
|
||||
print $"(_ansi red)❌ Guide file not found:(_ansi reset) ($full_path)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Display guide using best available viewer
|
||||
display_guide $full_path $guide_topic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Display guide using best available markdown viewer
|
||||
def display_guide [
|
||||
guide_path: path
|
||||
topic: string
|
||||
] {
|
||||
print $"\n(_ansi cyan_bold)📖 Guide:(_ansi reset) ($topic)\n"
|
||||
|
||||
# Check for viewers in order of preference: glow, bat, less, cat
|
||||
if (which glow | length) > 0 {
|
||||
^glow $guide_path
|
||||
} else if (which bat | length) > 0 {
|
||||
^bat --style=plain --paging=always $guide_path
|
||||
} else if (which less | length) > 0 {
|
||||
^less $guide_path
|
||||
} else {
|
||||
open $guide_path
|
||||
}
|
||||
}
|
||||
|
||||
# Show list of available guides
|
||||
def show_guide_list [docs_dir: path] {
|
||||
print $"
|
||||
(_ansi magenta_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
|
||||
(_ansi magenta_bold)║(_ansi reset) 📚 AVAILABLE GUIDES (_ansi magenta_bold)║(_ansi reset)
|
||||
(_ansi magenta_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[Step-by-Step Guides](_ansi reset)
|
||||
|
||||
(_ansi blue)provisioning guide from-scratch(_ansi reset)
|
||||
Complete deployment from zero to production
|
||||
(_ansi default_dimmed)Shortcuts: scratch, start, deploy(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[Quick References](_ansi reset)
|
||||
|
||||
(_ansi blue)provisioning guide quickstart(_ansi reset)
|
||||
Command shortcuts and quick reference
|
||||
(_ansi default_dimmed)Shortcuts: shortcuts, quick(_ansi reset)
|
||||
|
||||
(_ansi green_bold)USAGE(_ansi reset)
|
||||
|
||||
# View guide
|
||||
provisioning guide <topic>
|
||||
|
||||
# List all guides
|
||||
provisioning guide list
|
||||
provisioning howto (_ansi default_dimmed)# shortcut(_ansi reset)
|
||||
|
||||
(_ansi green_bold)EXAMPLES(_ansi reset)
|
||||
|
||||
# Complete deployment guide
|
||||
provisioning guide from-scratch
|
||||
|
||||
# Quick command reference
|
||||
provisioning guide quickstart
|
||||
|
||||
(_ansi green_bold)VIEWING TIPS(_ansi reset)
|
||||
|
||||
• (_ansi cyan)Best experience:(_ansi reset) Install glow for beautiful rendering
|
||||
(_ansi default_dimmed)brew install glow # macOS(_ansi reset)
|
||||
|
||||
• (_ansi cyan)Alternative:(_ansi reset) bat provides syntax highlighting
|
||||
(_ansi default_dimmed)brew install bat # macOS(_ansi reset)
|
||||
|
||||
• (_ansi cyan)Fallback:(_ansi reset) less/cat work on all systems
|
||||
|
||||
(_ansi default_dimmed)💡 All guides provide copy-paste ready commands
|
||||
Perfect for quick start and reference!(_ansi reset)
|
||||
"
|
||||
}
|
||||
78
nulib/cli/handlers/utilities/mod.nu
Normal file
78
nulib/cli/handlers/utilities/mod.nu
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Utilities Command Dispatcher
|
||||
# Routes utility commands to appropriate domain-specific handlers
|
||||
# NUSHELL 0.109 COMPLIANT - All handlers properly exported
|
||||
|
||||
use ./ssh.nu *
|
||||
use ./sops.nu *
|
||||
use ./cache.nu *
|
||||
use ./providers.nu *
|
||||
use ./plugins.nu *
|
||||
use ./shell.nu *
|
||||
use ./guides.nu *
|
||||
use ./qr.nu *
|
||||
use ./alias.nu *
|
||||
|
||||
# Main utility command dispatcher - Routes to appropriate domain handler
|
||||
export def handle_utility_command [
|
||||
command: string
|
||||
ops: string
|
||||
flags: record
|
||||
] {
|
||||
match $command {
|
||||
# Alias table (default: list)
|
||||
"alias" => {
|
||||
let action = ($ops | split row " " | first | default "list")
|
||||
match $action {
|
||||
"list" | "l" | "ls" | "" => { alias-list }
|
||||
_ => { alias-list }
|
||||
}
|
||||
}
|
||||
|
||||
# SSH operations
|
||||
"ssh" => { handle_ssh $flags }
|
||||
|
||||
# SOPS file editing (sed is alias)
|
||||
"sed" | "sops" => { handle_sops_edit $command $ops $flags }
|
||||
|
||||
# Cache management
|
||||
"cache" => { handle_cache $ops $flags }
|
||||
|
||||
# Provider management
|
||||
"providers" => { handle_providers $ops $flags }
|
||||
|
||||
# Plugin management
|
||||
"plugin" | "plugins" => { handle_plugins $ops $flags }
|
||||
|
||||
# Shell operations (nu, nuinfo, list)
|
||||
"nu" => { handle_nu $ops $flags }
|
||||
"nuinfo" => { handle_nuinfo }
|
||||
"list" | "l" | "ls" => { handle_list $ops $flags }
|
||||
|
||||
# Guide system
|
||||
"guide" | "guides" | "howto" => { handle_guide $ops $flags }
|
||||
|
||||
# QR code generation
|
||||
"qr" => { handle_qr }
|
||||
|
||||
# Unknown command
|
||||
_ => {
|
||||
print $"❌ Unknown utility command: ($command)"
|
||||
print ""
|
||||
print "Available utility commands:"
|
||||
print " ssh - SSH into server"
|
||||
print " sed - Edit SOPS encrypted files (alias)"
|
||||
print " sops - Edit SOPS encrypted files"
|
||||
print " cache - Cache management (status, config, clear, list)"
|
||||
print " providers - List available providers"
|
||||
print " nu - Start Nushell with provisioning library loaded"
|
||||
print " list - List resources (servers, taskservs, clusters)"
|
||||
print " qr - Generate QR code"
|
||||
print " nuinfo - Show Nushell version info"
|
||||
print " plugin - Plugin management (list, register, test, status)"
|
||||
print " guide - Show interactive guides (from-scratch, update, customize)"
|
||||
print ""
|
||||
print "Use 'provisioning help utilities' for more details"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
174
nulib/cli/handlers/utilities/plugins.nu
Normal file
174
nulib/cli/handlers/utilities/plugins.nu
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Plugin Command Handlers
|
||||
# Domain: Plugin discovery, installation, testing, and status
|
||||
|
||||
# Plugins command handler - Manage provisioning plugins
|
||||
export def handle_plugins [ops: string, flags: record] {
|
||||
let subcommand = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
"list"
|
||||
}
|
||||
|
||||
let remaining_ops = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | skip 1 | str join " ")
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
match $subcommand {
|
||||
"list" | "ls" => { handle_plugin_list $flags }
|
||||
"register" | "add" => { handle_plugin_register $remaining_ops $flags }
|
||||
"test" => { handle_plugin_test $remaining_ops $flags }
|
||||
"build" => { handle_plugin_build $remaining_ops $flags }
|
||||
"status" => { handle_plugin_status $flags }
|
||||
"help" => { show_plugin_help }
|
||||
_ => {
|
||||
print $"❌ Unknown plugin subcommand: ($subcommand)"
|
||||
print "Use 'provisioning plugin help' for available commands"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List installed plugins with status
|
||||
def handle_plugin_list [flags: record] {
|
||||
use platform/plugins/plugins.nu [list-plugins]
|
||||
|
||||
print $"\n (_ansi cyan_bold)Installed Plugins(_ansi reset)\n"
|
||||
|
||||
let plugins = (list-plugins)
|
||||
|
||||
if ($plugins | length) > 0 {
|
||||
print ($plugins | table -e)
|
||||
} else {
|
||||
print "(_ansi yellow)No plugins found(_ansi reset)"
|
||||
}
|
||||
|
||||
print $"\n(_ansi default_dimmed)💡 Use 'provisioning plugin register <name>' to register a plugin(_ansi reset)"
|
||||
}
|
||||
|
||||
# Register plugin with Nushell
|
||||
def handle_plugin_register [ops: string, flags: record] {
|
||||
use platform/plugins/plugins.nu [register-plugin]
|
||||
|
||||
let plugin_name = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
print $"(_ansi red)❌ Plugin name required(_ansi reset)"
|
||||
print $"Usage: provisioning plugin register <plugin_name>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
register-plugin $plugin_name
|
||||
}
|
||||
|
||||
# Test plugin functionality
|
||||
def handle_plugin_test [ops: string, flags: record] {
|
||||
use platform/plugins/plugins.nu [test-plugin]
|
||||
|
||||
let plugin_name = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
print $"(_ansi red)❌ Plugin name required(_ansi reset)"
|
||||
print $"Usage: provisioning plugin test <plugin_name>"
|
||||
print $"Valid plugins: auth, kms, tera, nickel"
|
||||
exit 1
|
||||
}
|
||||
|
||||
test-plugin $plugin_name
|
||||
}
|
||||
|
||||
# Build plugins from source
|
||||
def handle_plugin_build [ops: string, flags: record] {
|
||||
use platform/plugins/plugins.nu [build-plugins]
|
||||
|
||||
let plugin_name = if ($ops | is-not-empty) {
|
||||
($ops | split row " " | get 0)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if ($plugin_name | is-empty) {
|
||||
print $"\n(_ansi cyan)Building all plugins...(_ansi reset)"
|
||||
build-plugins
|
||||
} else {
|
||||
print $"\n(_ansi cyan)Building plugin: ($plugin_name)(_ansi reset)"
|
||||
build-plugins --plugin $plugin_name
|
||||
}
|
||||
}
|
||||
|
||||
# Show plugin status
|
||||
def handle_plugin_status [flags: record] {
|
||||
use platform/plugins/plugins.nu [plugin-build-info]
|
||||
use platform/plugins/auth.nu [plugin-auth-status]
|
||||
use platform/plugins_lib/kms.nu [plugin-kms-info]
|
||||
|
||||
print $"\n(_ansi cyan_bold)Plugin Status(_ansi reset)\n"
|
||||
|
||||
print $"(_ansi yellow_bold)Authentication Plugin:(_ansi reset)"
|
||||
let auth_status = (plugin-auth-status)
|
||||
print $" Available: ($auth_status.plugin_available)"
|
||||
print $" Enabled: ($auth_status.plugin_enabled)"
|
||||
print $" Mode: ($auth_status.mode)"
|
||||
|
||||
print $"\n(_ansi yellow_bold)KMS Plugin:(_ansi reset)"
|
||||
let kms_info = (plugin-kms-info)
|
||||
print $" Available: ($kms_info.plugin_available)"
|
||||
print $" Enabled: ($kms_info.plugin_enabled)"
|
||||
print $" Backend: ($kms_info.default_backend)"
|
||||
print $" Mode: ($kms_info.mode)"
|
||||
|
||||
print $"\n(_ansi yellow_bold)Build Information:(_ansi reset)"
|
||||
let build_info = (plugin-build-info)
|
||||
if $build_info.exists {
|
||||
print $" Source directory: ($build_info.plugins_dir)"
|
||||
print $" Available sources: ($build_info.available_sources | length)"
|
||||
} else {
|
||||
print $" Source directory: Not found"
|
||||
}
|
||||
}
|
||||
|
||||
# Show plugin help
|
||||
def show_plugin_help [] {
|
||||
print $"
|
||||
(_ansi cyan_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
|
||||
(_ansi cyan_bold)║(_ansi reset) 🔌 PLUGIN MANAGEMENT (_ansi cyan_bold)║(_ansi reset)
|
||||
(_ansi cyan_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[Plugin Operations](_ansi reset)
|
||||
(_ansi blue)plugin list(_ansi reset) List all plugins with status
|
||||
(_ansi blue)plugin register <name>(_ansi reset) Register plugin with Nushell
|
||||
(_ansi blue)plugin test <name>(_ansi reset) Test plugin functionality
|
||||
(_ansi blue)plugin build [name](_ansi reset) Build plugins from source
|
||||
(_ansi blue)plugin status(_ansi reset) Show plugin status and info
|
||||
|
||||
(_ansi green_bold)[Available Plugins](_ansi reset)
|
||||
• (_ansi cyan)auth(_ansi reset) - JWT authentication with MFA support
|
||||
• (_ansi cyan)kms(_ansi reset) - Key Management Service integration
|
||||
• (_ansi cyan)tera(_ansi reset) - Template rendering engine
|
||||
• (_ansi cyan)nickel(_ansi reset) - Nickel configuration language
|
||||
|
||||
(_ansi green_bold)EXAMPLES(_ansi reset)
|
||||
|
||||
# List all plugins
|
||||
provisioning plugin list
|
||||
|
||||
# Register auth plugin
|
||||
provisioning plugin register nu_plugin_auth
|
||||
|
||||
# Test KMS plugin
|
||||
provisioning plugin test kms
|
||||
|
||||
# Build all plugins
|
||||
provisioning plugin build
|
||||
|
||||
# Build specific plugin
|
||||
provisioning plugin build nu_plugin_auth
|
||||
|
||||
# Show plugin status
|
||||
provisioning plugin status
|
||||
|
||||
(_ansi default_dimmed)💡 Plugins provide HTTP fallback when not registered
|
||||
Authentication and KMS work in both plugin and HTTP modes(_ansi reset)
|
||||
"
|
||||
}
|
||||
491
nulib/cli/handlers/utilities/providers.nu
Normal file
491
nulib/cli/handlers/utilities/providers.nu
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
# Provider Command Handlers
|
||||
# Domain: Provider discovery, installation, removal, validation, and information
|
||||
|
||||
# REMOVED: use ../../../lib_provisioning * - causes circular import
|
||||
use cli/flags.nu *
|
||||
use primitives/io/interface.nu [_print _ansi]
|
||||
|
||||
# Validate identifier is safe from path/command injection
|
||||
def validate_safe_identifier [id: string] {
|
||||
# Returns true if INVALID (contains dangerous patterns)
|
||||
let has_slash = ($id | str contains "/")
|
||||
let has_dotdot = ($id | str contains "..")
|
||||
let starts_slash = ($id | str starts-with "/")
|
||||
let has_semicolon = ($id | str contains ";")
|
||||
let has_pipe = ($id | str contains "|")
|
||||
let has_ampersand = ($id | str contains "&")
|
||||
let has_dollar = ($id | str contains "$")
|
||||
let has_backtick = ($id | str contains "`")
|
||||
|
||||
if $has_slash or $has_dotdot or $starts_slash or $has_semicolon or $has_pipe or $has_ampersand or $has_dollar or $has_backtick {
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
# Main providers command handler - Manage infrastructure providers
|
||||
export def handle_providers [ops: string, flags: record] {
|
||||
|
||||
# Parse subcommand and arguments
|
||||
let parts = if ($ops | is-not-empty) {
|
||||
($ops | str trim | split row " " | where { |x| ($x | is-not-empty) })
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
let subcommand = if ($parts | length) > 0 { $parts | get 0 } else { "list" }
|
||||
let args = if ($parts | length) > 1 { $parts | skip 1 } else { [] }
|
||||
|
||||
match $subcommand {
|
||||
"list" => { handle_providers_list $flags $args }
|
||||
"info" => { handle_providers_info $args $flags }
|
||||
"install" => { handle_providers_install $args $flags }
|
||||
"remove" => { handle_providers_remove $args $flags }
|
||||
"installed" => { handle_providers_installed $args $flags }
|
||||
"validate" => { handle_providers_validate $args $flags }
|
||||
"help" | "-h" | "--help" => { show_providers_help }
|
||||
_ => {
|
||||
print $"❌ Unknown providers subcommand: ($subcommand)"
|
||||
print ""
|
||||
show_providers_help
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List all available providers
|
||||
def handle_providers_list [flags: record, args: list] {
|
||||
|
||||
_print $"(_ansi green)PROVIDERS(_ansi reset) list: \n"
|
||||
|
||||
# Parse flags
|
||||
let show_nickel = ($args | any { |x| $x == "--nickel" })
|
||||
let format_idx = ($args | enumerate | where item == "--format" | get 0?.index | default (-1))
|
||||
let format = if $format_idx >= 0 and ($args | length) > ($format_idx + 1) {
|
||||
$args | get ($format_idx + 1)
|
||||
} else {
|
||||
"table"
|
||||
}
|
||||
let no_cache = ($args | any { |x| $x == "--no-cache" })
|
||||
|
||||
# Get providers using cached Nickel module loader
|
||||
let providers = if $no_cache {
|
||||
(discover-nickel-modules "providers")
|
||||
} else {
|
||||
(discover-nickel-modules-cached "providers")
|
||||
}
|
||||
|
||||
match $format {
|
||||
"json" => {
|
||||
_print ($providers | to json) "json" "result" "table"
|
||||
}
|
||||
"yaml" => {
|
||||
_print ($providers | to yaml) "yaml" "result" "table"
|
||||
}
|
||||
_ => {
|
||||
# Table format - show summary or full with --nickel
|
||||
if $show_nickel {
|
||||
_print ($providers | to json) "json" "result" "table"
|
||||
} else {
|
||||
# Show simplified table
|
||||
let simplified = ($providers | each {|p|
|
||||
{name: $p.name, type: $p.type, version: $p.version}
|
||||
})
|
||||
_print ($simplified | to json) "json" "result" "table"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Show detailed provider information
|
||||
def handle_providers_info [args: list, flags: record] {
|
||||
|
||||
if ($args | is-empty) {
|
||||
print "❌ Provider name required"
|
||||
print "Usage: provisioning providers info <provider> [--nickel] [--no-cache]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let provider_name = $args | get 0
|
||||
|
||||
# Validate provider name
|
||||
if (validate_safe_identifier $provider_name) {
|
||||
error make { msg: "Invalid provider name - contains invalid characters" }
|
||||
}
|
||||
|
||||
let show_nickel = ($args | any { |x| $x == "--nickel" })
|
||||
let no_cache = ($args | any { |x| $x == "--no-cache" })
|
||||
|
||||
print $"(_ansi blue_bold)📋 Provider Information: ($provider_name)(_ansi reset)"
|
||||
print ""
|
||||
|
||||
let providers = if $no_cache {
|
||||
(discover-nickel-modules "providers")
|
||||
} else {
|
||||
(discover-nickel-modules-cached "providers")
|
||||
}
|
||||
let provider_info = ($providers | where name == $provider_name)
|
||||
|
||||
if ($provider_info | is-empty) {
|
||||
print $"❌ Provider not found: ($provider_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let info = ($provider_info | first)
|
||||
|
||||
print $" Name: ($info.name)"
|
||||
print $" Type: ($info.type)"
|
||||
print $" Path: ($info.path)"
|
||||
print $" Has Nickel: ($info.has_nickel)"
|
||||
|
||||
if $show_nickel and $info.has_nickel {
|
||||
print ""
|
||||
print " (_ansi cyan_bold)Nickel Module:(_ansi reset)"
|
||||
print $" Module Name: ($info.module_name)"
|
||||
print $" Nickel Path: ($info.schema_path)"
|
||||
print $" Version: ($info.version)"
|
||||
print $" Edition: ($info.edition)"
|
||||
|
||||
# Check for nickel.mod file
|
||||
let decl_mod = ($info.schema_path | path join "nickel.mod")
|
||||
if ($decl_mod | path exists) {
|
||||
print ""
|
||||
print $" (_ansi cyan_bold)nickel.mod content:(_ansi reset)"
|
||||
open $decl_mod | lines | each {|line| print $" ($line)"}
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
}
|
||||
|
||||
# Install provider for infrastructure
|
||||
def handle_providers_install [args: list, flags: record] {
|
||||
|
||||
if ($args | length) < 2 {
|
||||
print "❌ Provider name and infrastructure required"
|
||||
print "Usage: provisioning providers install <provider> <infra> [--version <v>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let provider_name = $args | get 0
|
||||
let infra_name = $args | get 1
|
||||
|
||||
# Validate provider and infrastructure names
|
||||
if (validate_safe_identifier $provider_name) {
|
||||
error make { msg: "Invalid provider name - contains invalid characters" }
|
||||
}
|
||||
if (validate_safe_identifier $infra_name) {
|
||||
error make { msg: "Invalid infrastructure name - contains invalid characters" }
|
||||
}
|
||||
|
||||
# Extract version flag if present
|
||||
let version_idx = ($args | enumerate | where item == "--version" | get 0?.index | default (-1))
|
||||
let version = if $version_idx >= 0 and ($args | length) > ($version_idx + 1) {
|
||||
$args | get ($version_idx + 1)
|
||||
} else {
|
||||
"0.0.1"
|
||||
}
|
||||
|
||||
# Resolve infrastructure path
|
||||
let infra_path = (resolve_infra_path $infra_name)
|
||||
|
||||
if ($infra_path | is-empty) {
|
||||
print $"❌ Infrastructure not found: ($infra_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Install provider
|
||||
install-provider $provider_name $infra_path --version $version
|
||||
|
||||
print ""
|
||||
print $"(_ansi yellow_bold)💡 Next steps:(_ansi reset)"
|
||||
print $" 1. Check the manifest: ($infra_path)/providers.manifest.yaml"
|
||||
print $" 2. Update server definitions to use ($provider_name)"
|
||||
print $" 3. Run: nickel run defs/servers.ncl"
|
||||
}
|
||||
|
||||
# Remove provider from infrastructure
|
||||
def handle_providers_remove [args: list, flags: record] {
|
||||
|
||||
if ($args | length) < 2 {
|
||||
print "❌ Provider name and infrastructure required"
|
||||
print "Usage: provisioning providers remove <provider> <infra> [--force]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let provider_name = $args | get 0
|
||||
let infra_name = $args | get 1
|
||||
|
||||
# Validate provider and infrastructure names
|
||||
if (validate_safe_identifier $provider_name) {
|
||||
error make { msg: "Invalid provider name - contains invalid characters" }
|
||||
}
|
||||
if (validate_safe_identifier $infra_name) {
|
||||
error make { msg: "Invalid infrastructure name - contains invalid characters" }
|
||||
}
|
||||
|
||||
let force = ($args | any { |x| $x == "--force" })
|
||||
|
||||
# Resolve infrastructure path
|
||||
let infra_path = (resolve_infra_path $infra_name)
|
||||
|
||||
if ($infra_path | is-empty) {
|
||||
print $"❌ Infrastructure not found: ($infra_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Confirmation unless forced
|
||||
if not $force {
|
||||
print $"(_ansi yellow)⚠️ This will remove provider ($provider_name) from ($infra_name)(_ansi reset)"
|
||||
print " Nickel dependencies will be updated."
|
||||
let response = (input "Continue? (y/N): ")
|
||||
|
||||
if ($response | str downcase) != "y" {
|
||||
print "❌ Cancelled"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Remove provider
|
||||
remove-provider $provider_name $infra_path
|
||||
}
|
||||
|
||||
# List installed providers for infrastructure
|
||||
def handle_providers_installed [args: list, flags: record] {
|
||||
if ($args | is-empty) {
|
||||
print "❌ Infrastructure name required"
|
||||
print "Usage: provisioning providers installed <infra> [--format <fmt>]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let infra_name = $args | get 0
|
||||
|
||||
# Validate infrastructure name
|
||||
if (validate_safe_identifier $infra_name) {
|
||||
error make { msg: "Invalid infrastructure name - contains invalid characters" }
|
||||
}
|
||||
|
||||
# Parse format flag
|
||||
let format_idx = ($args | enumerate | where item == "--format" | get 0?.index | default (-1))
|
||||
let format = if $format_idx >= 0 and ($args | length) > ($format_idx + 1) {
|
||||
$args | get ($format_idx + 1)
|
||||
} else {
|
||||
"table"
|
||||
}
|
||||
|
||||
# Resolve infrastructure path
|
||||
let infra_path = (resolve_infra_path $infra_name)
|
||||
|
||||
if ($infra_path | is-empty) {
|
||||
print $"❌ Infrastructure not found: ($infra_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let manifest_path = ($infra_path | path join "providers.manifest.yaml")
|
||||
|
||||
if not ($manifest_path | path exists) {
|
||||
print $"❌ No providers.manifest.yaml found in ($infra_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let manifest = (open $manifest_path)
|
||||
let providers = if ($manifest | get providers? | is-not-empty) {
|
||||
$manifest | get providers
|
||||
} else if ($manifest | get loaded_providers? | is-not-empty) {
|
||||
$manifest | get loaded_providers
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
print $"(_ansi blue_bold)📦 Installed providers for ($infra_name):(_ansi reset)"
|
||||
print ""
|
||||
|
||||
match $format {
|
||||
"json" => {
|
||||
_print ($providers | to json) "json" "result" "table"
|
||||
}
|
||||
"yaml" => {
|
||||
_print ($providers | to yaml) "yaml" "result" "table"
|
||||
}
|
||||
_ => {
|
||||
_print ($providers | to json) "json" "result" "table"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Validate provider installation
|
||||
def handle_providers_validate [args: list, flags: record] {
|
||||
|
||||
if ($args | is-empty) {
|
||||
print "❌ Infrastructure name required"
|
||||
print "Usage: provisioning providers validate <infra> [--no-cache]"
|
||||
exit 1
|
||||
}
|
||||
|
||||
let infra_name = $args | get 0
|
||||
|
||||
# Validate infrastructure name
|
||||
if (validate_safe_identifier $infra_name) {
|
||||
error make { msg: "Invalid infrastructure name - contains invalid characters" }
|
||||
}
|
||||
|
||||
let no_cache = ($args | any { |x| $x == "--no-cache" })
|
||||
|
||||
print $"(_ansi blue_bold)🔍 Validating providers for ($infra_name)...(_ansi reset)"
|
||||
print ""
|
||||
|
||||
# Resolve infrastructure path
|
||||
let infra_path = (resolve_infra_path $infra_name)
|
||||
|
||||
if ($infra_path | is-empty) {
|
||||
print $"❌ Infrastructure not found: ($infra_name)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Refactored from mutable to immutable accumulation (Rule 3)
|
||||
let validation_result = (
|
||||
# Check manifest exists
|
||||
let manifest_path = ($infra_path | path join "providers.manifest.yaml");
|
||||
let initial = {has_manifest: false, errors: []};
|
||||
|
||||
if not ($manifest_path | path exists) {
|
||||
$initial | upsert has_manifest false | upsert errors ["providers.manifest.yaml not found"]
|
||||
} else {
|
||||
# Check each provider in manifest
|
||||
let manifest = (open $manifest_path)
|
||||
let providers = ($manifest | get providers? | default [])
|
||||
|
||||
# Load providers once using cache
|
||||
let all_providers = if $no_cache {
|
||||
(discover-nickel-modules "providers")
|
||||
} else {
|
||||
(discover-nickel-modules-cached "providers")
|
||||
}
|
||||
|
||||
# Use reduce --fold to accumulate validation errors (Rule 3)
|
||||
let validation = ($providers | reduce --fold {errors: []} {|provider, result|
|
||||
print $" Checking ($provider.name)..."
|
||||
|
||||
# Check if provider exists in cached list
|
||||
let available = ($all_providers | where name == $provider.name)
|
||||
|
||||
if ($available | is-empty) {
|
||||
$result | upsert errors ($result.errors | append $"Provider not found: ($provider.name)")
|
||||
print $" ❌ Not found in extensions"
|
||||
} else {
|
||||
let provider_info = ($available | first)
|
||||
|
||||
# Check if symlink exists
|
||||
let modules_dir = ($infra_path | path join ".nickel-modules")
|
||||
let link_path = ($modules_dir | path join $provider_info.module_name)
|
||||
|
||||
if not ($link_path | path exists) {
|
||||
$result | upsert errors ($result.errors | append $"Symlink missing: ($link_path)")
|
||||
print $" ❌ Symlink not found"
|
||||
} else {
|
||||
print $" ✓ OK"
|
||||
$result
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Check nickel.mod
|
||||
let nickel_mod_path = ($infra_path | path join "nickel.mod")
|
||||
let final_errors = if not ($nickel_mod_path | path exists) {
|
||||
($validation.errors | append "nickel.mod not found")
|
||||
} else {
|
||||
$validation.errors
|
||||
}
|
||||
|
||||
$initial | upsert has_manifest true | upsert errors $final_errors
|
||||
}
|
||||
)
|
||||
|
||||
print ""
|
||||
|
||||
# Report results
|
||||
if ($validation_result.errors | is-empty) {
|
||||
print "(_ansi green)✅ Validation passed - all providers correctly installed(_ansi reset)"
|
||||
} else {
|
||||
print "(_ansi red)❌ Validation failed:(_ansi reset)"
|
||||
$validation_result.errors | each {|error| print $" • ($error)"}
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: Resolve infrastructure path
|
||||
def resolve_infra_path [infra: string] {
|
||||
if ($infra | path exists) {
|
||||
return $infra
|
||||
}
|
||||
|
||||
# Try workspace/infra path
|
||||
let workspace_path = $"workspace/infra/($infra)"
|
||||
if ($workspace_path | path exists) {
|
||||
return $workspace_path
|
||||
}
|
||||
|
||||
# Try absolute workspace path
|
||||
let proj_root = ($env.PROVISIONING_ROOT? | default ($env.HOME | path join "Development/provisioning"))
|
||||
let abs_workspace_path = ($proj_root | path join "workspace" "infra" $infra)
|
||||
if ($abs_workspace_path | path exists) {
|
||||
return $abs_workspace_path
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
# Show providers help
|
||||
def show_providers_help [] {
|
||||
print $"
|
||||
(_ansi cyan_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
|
||||
(_ansi cyan_bold)║(_ansi reset) 📦 PROVIDER MANAGEMENT (_ansi cyan_bold)║(_ansi reset)
|
||||
(_ansi cyan_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
|
||||
|
||||
(_ansi green_bold)[Available Providers](_ansi reset)
|
||||
(_ansi blue)provisioning providers list [--nickel] [--format <fmt>](_ansi reset)
|
||||
List all available providers
|
||||
Formats: table (default value), json, yaml
|
||||
|
||||
(_ansi blue)provisioning providers info <provider> [--nickel](_ansi reset)
|
||||
Show detailed provider information with optional Nickel details
|
||||
|
||||
(_ansi green_bold)[Provider Installation](_ansi reset)
|
||||
(_ansi blue)provisioning providers install <provider> <infra> [--version <v>](_ansi reset)
|
||||
Install provider for an infrastructure
|
||||
Default version: 0.0.1
|
||||
|
||||
(_ansi blue)provisioning providers remove <provider> <infra> [--force](_ansi reset)
|
||||
Remove provider from infrastructure
|
||||
--force skips confirmation prompt
|
||||
|
||||
(_ansi blue)provisioning providers installed <infra> [--format <fmt>](_ansi reset)
|
||||
List installed providers for infrastructure
|
||||
Formats: table (default value), json, yaml
|
||||
|
||||
(_ansi blue)provisioning providers validate <infra>(_ansi reset)
|
||||
Validate provider installation and configuration
|
||||
|
||||
(_ansi green_bold)EXAMPLES(_ansi reset)
|
||||
|
||||
# List all providers
|
||||
provisioning providers list
|
||||
|
||||
# Show Nickel module details
|
||||
provisioning providers info upcloud --nickel
|
||||
|
||||
# Install provider
|
||||
provisioning providers install upcloud myinfra
|
||||
|
||||
# List installed providers
|
||||
provisioning providers installed myinfra
|
||||
|
||||
# Validate installation
|
||||
provisioning providers validate myinfra
|
||||
|
||||
# Remove provider
|
||||
provisioning providers remove aws myinfra --force
|
||||
|
||||
(_ansi default_dimmed)💡 Use 'provisioning help providers' for more information(_ansi reset)
|
||||
"
|
||||
}
|
||||
9
nulib/cli/handlers/utilities/qr.nu
Normal file
9
nulib/cli/handlers/utilities/qr.nu
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# QR Code Command Handler
|
||||
# Domain: QR code generation
|
||||
|
||||
# REMOVED: use ../../../lib_provisioning * - causes circular import
|
||||
|
||||
# QR code command handler - Generate QR code
|
||||
export def handle_qr [] {
|
||||
make_qr
|
||||
}
|
||||
108
nulib/cli/handlers/utilities/shell.nu
Normal file
108
nulib/cli/handlers/utilities/shell.nu
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Shell Command Handlers
|
||||
# Domain: Nushell environment, shell info, and resource listing
|
||||
|
||||
# REMOVED: use ../../../lib_provisioning * - causes circular import
|
||||
use cli/flags.nu *
|
||||
|
||||
# Validate infrastructure name is safe from path injection
|
||||
def validate_infra_name [infra: string] {
|
||||
# Returns true if INVALID (contains dangerous patterns)
|
||||
if ($infra | str contains "/") or ($infra | str contains "..") or ($infra | str starts-with "/") or ($infra | str contains " ") {
|
||||
return true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
# Nu shell command handler - Start Nushell with provisioning library loaded
|
||||
export def handle_nu [ops: string, flags: record] {
|
||||
let run_ops = if ($ops | str trim | str starts-with "-") {
|
||||
""
|
||||
} else {
|
||||
let parts = ($ops | split row " ")
|
||||
if ($parts | is-empty) { "" } else { $parts | first }
|
||||
}
|
||||
|
||||
if ($flags.infra | is-not-empty) {
|
||||
# Validate infra name to prevent path injection
|
||||
if (validate_infra_name $flags.infra) {
|
||||
error make { msg: "Invalid infrastructure name - contains path traversal characters" }
|
||||
}
|
||||
if ($env.PROVISIONING_INFRA_PATH | path join $flags.infra | path exists) {
|
||||
cd ($env.PROVISIONING_INFRA_PATH | path join $flags.infra)
|
||||
}
|
||||
}
|
||||
|
||||
if ($flags.output_format | is-empty) {
|
||||
if ($run_ops | is-empty) {
|
||||
print (
|
||||
$"\nTo exit (_ansi purple_bold)NuShell(_ansi reset) session, with (_ansi default_dimmed)lib_provisioning(_ansi reset) loaded, " +
|
||||
$"use (_ansi green_bold)exit(_ansi reset) or (_ansi green_bold)[CTRL-D](_ansi reset)"
|
||||
)
|
||||
# Pass the provisioning configuration files to the Nu subprocess
|
||||
# This ensures the interactive session has the same config loaded as the calling environment
|
||||
let config_path = ($env.PROVISIONING_CONFIG? | default "")
|
||||
# Build library paths argument - needed for module resolution during parsing
|
||||
# Convert colon-separated string to -I flag arguments
|
||||
let lib_dirs = ($env.NU_LIB_DIRS? | default "")
|
||||
let lib_paths = if ($lib_dirs | is-not-empty) {
|
||||
($lib_dirs | split row ":" | where { |x| ($x | is-not-empty) })
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
if ($config_path | is-not-empty) {
|
||||
# Pass config files AND library paths via -I flags for module resolution
|
||||
# Library paths are set via -I flags which enables module resolution during parsing phase
|
||||
if ($lib_paths | length) > 0 {
|
||||
# Construct command with -I flags for each library path
|
||||
let cmd = (mut cmd_parts = []; for path in $lib_paths { $cmd_parts = ($cmd_parts | append "-I" | append $path) }; $cmd_parts)
|
||||
# Start interactive Nushell with provisioning configuration loaded
|
||||
# The -i flag enables interactive mode (REPL) with full terminal features
|
||||
^nu --config $"($config_path)/config.nu" --env-config $"($config_path)/env.nu" ...$cmd -i
|
||||
} else {
|
||||
^nu --config $"($config_path)/config.nu" --env-config $"($config_path)/env.nu" -i
|
||||
}
|
||||
} else {
|
||||
# Fallback if PROVISIONING_CONFIG not set
|
||||
if ($lib_paths | length) > 0 {
|
||||
let cmd = (mut cmd_parts = []; for path in $lib_paths { $cmd_parts = ($cmd_parts | append "-I" | append $path) }; $cmd_parts)
|
||||
^nu ...$cmd -i
|
||||
} else {
|
||||
^nu -i
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# Also pass library paths for single command execution
|
||||
let lib_dirs = ($env.NU_LIB_DIRS? | default "")
|
||||
let lib_paths = if ($lib_dirs | is-not-empty) {
|
||||
($lib_dirs | split row ":" | where { |x| ($x | is-not-empty) })
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
if ($lib_paths | length) > 0 {
|
||||
let cmd = (mut cmd_parts = []; for path in $lib_paths { $cmd_parts = ($cmd_parts | append "-I" | append $path) }; $cmd_parts)
|
||||
^nu ...$cmd -c $"($run_ops)"
|
||||
} else {
|
||||
^nu -c $"($run_ops)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Nu info command handler - Show Nushell version info
|
||||
export def handle_nuinfo [] {
|
||||
print $"\n (_ansi yellow)Nu shell info(_ansi reset)"
|
||||
print (version)
|
||||
}
|
||||
|
||||
# List command handler - List resources (servers, taskservs, clusters)
|
||||
export def handle_list [ops: string, flags: record] {
|
||||
let target_list = if ($ops | is-not-empty) {
|
||||
let parts = ($ops | split row " ")
|
||||
if ($parts | is-empty) { "" } else { $parts | first }
|
||||
} else { "" }
|
||||
|
||||
let list_ops = ($ops | str replace $"($target_list) " "" | str trim)
|
||||
on_list $target_list ($flags.onsel | default "") $list_ops
|
||||
}
|
||||
43
nulib/cli/handlers/utilities/sops.nu
Normal file
43
nulib/cli/handlers/utilities/sops.nu
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# SOPS Command Handler
|
||||
# Domain: SOPS encrypted file editing
|
||||
|
||||
# REMOVED: use ../../../lib_provisioning * - causes circular import
|
||||
|
||||
# SOPS edit command handler - Edit SOPS encrypted files (sed is alias)
|
||||
export def handle_sops_edit [task: string, ops: string, flags: record] {
|
||||
let pos = if $task == "sed" { 0 } else { 1 }
|
||||
let ops_parts = ($ops | split row " ")
|
||||
let target_file = if ($ops_parts | length) > $pos { $ops_parts | get $pos } else { "" }
|
||||
|
||||
if ($target_file | is-empty) {
|
||||
throw-error $"🛑 No file found" $"for (_ansi yellow_bold)sops(_ansi reset) edit"
|
||||
exit -1
|
||||
}
|
||||
|
||||
let target_full_path = if not ($target_file | path exists) {
|
||||
let infra_path = (get_infra $flags.infra)
|
||||
let candidate = ($infra_path | path join $target_file)
|
||||
if ($candidate | path exists) {
|
||||
$candidate
|
||||
} else {
|
||||
throw-error $"🛑 No file (_ansi green_italic)($target_file)(_ansi reset) found" $"for (_ansi yellow_bold)sops(_ansi reset) edit"
|
||||
exit -1
|
||||
}
|
||||
} else {
|
||||
$target_file
|
||||
}
|
||||
|
||||
# Setup SOPS environment if needed
|
||||
if ($env.PROVISIONING_SOPS? | is-empty) {
|
||||
let curr_settings = (find_get_settings --infra $flags.infra --settings $flags.settings $flags.include_notuse)
|
||||
rm -rf $curr_settings.wk_path
|
||||
$env.CURRENT_INFRA_PATH = ($curr_settings.infra_path | path join $curr_settings.infra)
|
||||
use platform/sops/lib.nu [on_sops]
|
||||
}
|
||||
|
||||
if $task == "sed" {
|
||||
on_sops "sed" $target_full_path
|
||||
} else {
|
||||
on_sops $task $target_full_path ($ops_parts | skip 1)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue