feat(core): three-layer DAG, unified component arch, commands-registry cache, Nushell 0.112.2 migration
- DAG architecture: `dag show/validate/export` (nulib/main_provisioning/dag.nu),
config loader (lib_provisioning/config/loader/dag.nu), taskserv dag-executor.
Backed by schemas/lib/dag/*.ncl; orchestrator emits NATS events via
WorkspaceComposition::into_workflow. See ADR-020, ADR-021.
- Unified Component Architecture: components/mod.nu, main_provisioning/
{components,workflow,extensions,ontoref-queries}.nu. Full workflow engine with
topological sort and NATS subject emission. Blocks A-H complete (libre-daoshi).
- Commands-registry: nulib/commands-registry.ncl (Nickel source, 314 lines) +
JSON cache at ~/.cache/provisioning/commands-registry.json rebuilt on source
change. cli/provisioning fast-path alias expansion avoids cold Nu startup.
ADDING_COMMANDS.md documents new-command workflow.
- Platform service manager: service-manager.nu (+573), startup.nu (+611),
service-check.nu (+255); autostart/bootstrap/health/target refactored.
- Nushell 0.112.2 migration: removed all try/catch and bash redirections;
external commands prefixed with ^; type signatures enforced. Driven by
scripts/refactor-try-catch{,-simplified}.nu.
- TTY stack: removed shlib/*-tty.sh; replaced by cli/tty-dispatch.sh,
tty-filter.sh, tty-commands.conf.
- New domain modules: images/ (golden image lifecycle), workspace/{state,sync}.nu,
main_provisioning/{bootstrap,cluster-deploy,fip,state}.nu, commands/{state,
build,integrations/auth,utilities/alias}.nu, platform.nu expanded (+874).
- Config loader overhaul: loader/core.nu slimmed (-759), cache/core.nu
refactored (-454), removed legacy loaders/file_loader.nu (-330).
- Thirteen new provisioning-<domain>.nu top-level modules for bash dispatcher.
- Tests: test_workspace_state.nu (+351); updates to test_oci_registry,
test_services.
- README + CHANGELOG updated.
This commit is contained in:
parent
adb28be45a
commit
894046ef5a
245 changed files with 22277 additions and 6121 deletions
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "5246326f-910e-4f2d-aef2-df29d0cbeeca",
|
||||||
|
"name": "execute_servers_script_",
|
||||||
|
"command": "bash",
|
||||||
|
"args": [
|
||||||
|
"-c",
|
||||||
|
"base64 -d < /tmp/orchestrator_script_5246326f-910e-4f2d-aef2-df29d0cbeeca.tar.gz.b64 | gunzip | tar -xOf - script.sh | bash +x"
|
||||||
|
],
|
||||||
|
"dependencies": [],
|
||||||
|
"status": "Failed",
|
||||||
|
"created_at": "2026-02-17T00:50:47.638979Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": "2026-02-17T00:50:49.815378Z",
|
||||||
|
"output": null,
|
||||||
|
"error": "Command execution failed: === Checking prerequisites ===\n✓ HCLOUD_TOKEN set\n\n=== Managing SSH Keys ===\n✓ SSH public key found: /Users/jesusperezlorenzo/.ssh/htz_ops.pub\nChecking if SSH key 'htz_ops' exists in Hetzner...\n✓ SSH key 'htz_ops' already exists with ID: 106168627\n\n=== SSH Key Management Complete ===\nSSH_KEY_ID: 106168627\nState saved to: /tmp/.provisioning-state.json\nEnvironment variables exported to: /tmp/.env\n=== Checking prerequisites ===\n✓ Prerequisites satisfied\n\n=== Managing Network ===\n✓ Network config validated: 10.0.0.0/16 with subnet 10.0.0.0/22 in zone eu-central\nChecking if network 'librecloud-private' exists...\nCreating network 'librecloud-private' with IP range 10.0.0.0/16 (with protection enabled)...\n✓ Network 'librecloud-private' created with ID: 11943075\nCreating subnet with IP range 10.0.0.0/22 in network 11943075...\nERROR: Failed to create subnet\nResponse: hcloud: unknown shorthand flag: 'o' in -o\n"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "881402e9-8851-4c3e-a988-5cf758d62803",
|
||||||
|
"name": "execute_servers_script_",
|
||||||
|
"command": "bash",
|
||||||
|
"args": [
|
||||||
|
"-c",
|
||||||
|
"base64 -d < /tmp/orchestrator_script_881402e9-8851-4c3e-a988-5cf758d62803.tar.gz.b64 | gunzip | tar -xOf - script.sh | bash +x"
|
||||||
|
],
|
||||||
|
"dependencies": [],
|
||||||
|
"status": "Failed",
|
||||||
|
"created_at": "2026-02-17T00:13:37.519543Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": "2026-02-17T00:13:39.388566Z",
|
||||||
|
"output": null,
|
||||||
|
"error": "Command execution failed: === Checking prerequisites ===\n✓ HCLOUD_TOKEN set\n\n=== Managing SSH Keys ===\n✓ SSH public key found: /Users/jesusperezlorenzo/.ssh/htz_ops.pub\nChecking if SSH key 'htz_ops' exists in Hetzner...\n✓ SSH key 'htz_ops' already exists with ID: 106168627\n\n=== SSH Key Management Complete ===\nSSH_KEY_ID: 106168627\nState saved to: /tmp/.provisioning-state.json\nEnvironment variables exported to: /tmp/.env\n=== Checking prerequisites ===\n✓ Prerequisites satisfied\n\n=== Managing Network ===\n✓ Network config validated: 10.0.0.0/16 with subnet 10.0.0.0/22 in zone eu-central\nChecking if network 'librecloud-private' exists...\nCreating network 'librecloud-private' with IP range 10.0.0.0/16 (with protection enabled)...\n✓ Network 'librecloud-private' created with ID: 11943026\nCreating subnet with IP range 10.0.0.0/22 in network 11943026...\nERROR: Failed to create subnet\nResponse: hcloud: unknown shorthand flag: 'o' in -o\n"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"id": "d2643c9b-dd6e-42f9-9d72-0a767b5c308c",
|
||||||
|
"name": "execute_servers_script_",
|
||||||
|
"command": "bash",
|
||||||
|
"args": [
|
||||||
|
"-c",
|
||||||
|
"base64 -d < /tmp/orchestrator_script_d2643c9b-dd6e-42f9-9d72-0a767b5c308c.tar.gz.b64 | gunzip | tar -xOf - script.sh | bash +x"
|
||||||
|
],
|
||||||
|
"dependencies": [],
|
||||||
|
"status": "Failed",
|
||||||
|
"created_at": "2026-02-16T23:52:45.294780Z",
|
||||||
|
"started_at": null,
|
||||||
|
"completed_at": "2026-02-16T23:52:47.796824Z",
|
||||||
|
"output": null,
|
||||||
|
"error": "Command execution failed: hcloud: invalid input in field 'networks' (invalid_input, f966df9ad630cc87f7f495a9502858b1)\n- networks: networks must have at least one subnetwork\n"
|
||||||
|
}
|
||||||
|
|
@ -134,8 +134,10 @@ repos:
|
||||||
# exclude: ^\.woodpecker/
|
# exclude: ^\.woodpecker/
|
||||||
|
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
exclude: ^(\.coder/|\.wrks/|\.claude/)
|
||||||
|
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude: \.md$
|
exclude: \.md$|^(\.coder/|\.wrks/|\.claude/)
|
||||||
|
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
|
exclude: ^(\.coder/|\.wrks/|\.claude/)
|
||||||
|
|
|
||||||
150
CHANGELOG.md
150
CHANGELOG.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Provisioning Core - Changelog
|
# Provisioning Core - Changelog
|
||||||
|
|
||||||
**Date**: 2026-01-14
|
**Date**: 2026-04-17
|
||||||
**Repository**: provisioning/core
|
**Repository**: provisioning/core
|
||||||
**Status**: Nickel IaC (PRIMARY)
|
**Status**: Nickel IaC (PRIMARY)
|
||||||
|
|
||||||
|
|
@ -8,12 +8,154 @@
|
||||||
|
|
||||||
## 📋 Summary
|
## 📋 Summary
|
||||||
|
|
||||||
Core system with Nickel as primary IaC: Terminology migration from cluster to taskserv throughout codebase,
|
Major refactor: three-layer DAG architecture with workspace composition, Unified
|
||||||
Nushell library refactoring for improved ANSI output formatting, and enhanced handler modules for infrastructure operations.
|
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-01-14)
|
## 🔄 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
|
### Terminology Migration: Cluster → Taskserv
|
||||||
|
|
||||||
|
|
|
||||||
122
README.md
122
README.md
|
|
@ -28,48 +28,60 @@ The Core Engine provides:
|
||||||
```text
|
```text
|
||||||
provisioning/core/
|
provisioning/core/
|
||||||
├── cli/ # Command-line interface
|
├── cli/ # Command-line interface
|
||||||
│ └── provisioning # Main CLI entry point (211 lines, 84% reduction)
|
│ ├── provisioning # Main bash wrapper (command-registry cache aware)
|
||||||
|
│ ├── tty-dispatch.sh # TTY-safe dispatcher (replaces shlib)
|
||||||
|
│ ├── tty-filter.sh # TTY command filter
|
||||||
|
│ └── tty-commands.conf # TTY command manifest
|
||||||
├── nulib/ # Core Nushell libraries
|
├── nulib/ # Core Nushell libraries
|
||||||
│ ├── lib_provisioning/ # Core provisioning libraries
|
│ ├── commands-registry.ncl # Command catalog (Nickel → JSON cache)
|
||||||
│ │ ├── config/ # Configuration loading and management
|
│ ├── lib_provisioning/ # Core provisioning libraries
|
||||||
│ │ ├── utils/ # Utility functions (SSH, validation, logging)
|
│ │ ├── config/ # Hierarchical loader, cache, DAG loader
|
||||||
│ │ ├── providers/ # Provider abstraction layer
|
│ │ ├── platform/ # Service manager, startup, bootstrap, health
|
||||||
│ │ ├── secrets/ # Secrets management (SOPS integration)
|
│ │ ├── utils/ # SSH, logging, nickel_processor, path-utils
|
||||||
│ │ ├── workspace/ # Workspace management
|
│ │ ├── plugins/ # auth, kms, orchestrator, secretumvault
|
||||||
│ │ └── infra_validator/ # Infrastructure validation engine
|
│ │ ├── providers/ # Provider registry and loader
|
||||||
│ ├── main_provisioning/ # CLI command handlers
|
│ │ ├── workspace/ # Workspace config, verification, enforcement
|
||||||
│ │ ├── flags.nu # Centralized flag handling
|
│ │ └── infra_validator/ # Schema-aware validation engine
|
||||||
│ │ ├── dispatcher.nu # Command routing (80+ shortcuts)
|
│ ├── main_provisioning/ # CLI command handlers
|
||||||
│ │ ├── help_system.nu # Categorized help system
|
│ │ ├── dispatcher.nu # Command routing (80+ shortcuts)
|
||||||
│ │ └── commands/ # Domain-focused command modules
|
│ │ ├── dag.nu # `dag show/validate/export`
|
||||||
|
│ │ ├── components.nu # Components + capabilities queries
|
||||||
|
│ │ ├── workflow.nu # Workflow engine (topo sort, NATS events)
|
||||||
|
│ │ ├── bootstrap.nu # Platform bootstrap
|
||||||
|
│ │ ├── cluster-deploy.nu # Component/taskserv dispatch
|
||||||
|
│ │ ├── ontoref-queries.nu # on+re-aware CLI queries
|
||||||
|
│ │ └── commands/ # Domain-focused command modules
|
||||||
|
│ ├── components/ # Component dispatch module (NEW)
|
||||||
|
│ ├── images/ # Golden image lifecycle (create/list/update/watch)
|
||||||
│ ├── servers/ # Server management modules
|
│ ├── servers/ # Server management modules
|
||||||
│ ├── taskservs/ # Task service modules
|
│ ├── taskservs/ # Task service modules (+ dag-executor)
|
||||||
│ ├── clusters/ # Cluster management modules
|
│ ├── clusters/ # Cluster management modules
|
||||||
│ └── workflows/ # Workflow orchestration modules
|
│ ├── workflows/ # Workflow orchestration modules
|
||||||
├── scripts/ # Utility scripts
|
│ ├── workspace/ # Workspace state + sync
|
||||||
│ └── test/ # Test automation
|
│ └── scripts/ # In-repo nushell scripts (query-*, validate-*)
|
||||||
└── resources/ # Images and logos
|
├── scripts/ # Utility scripts (refactor, deploy, manage-ports)
|
||||||
|
└── services/ # Service definitions
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- **Nushell 0.109.0+** - Primary shell and scripting environment
|
- **Nushell 0.112.2** - Primary shell and scripting environment
|
||||||
- **Nickel 1.15.1+** - Configuration language for infrastructure definitions
|
- **Nickel 1.15.1+** - Configuration language for infrastructure definitions
|
||||||
- **SOPS 3.10.2+** - Secrets management (optional but recommended)
|
- **SOPS 3.10.2+** - Secrets management (optional but recommended)
|
||||||
- **Age 1.2.1+** - Encryption tool for secrets (optional)
|
- **Age 1.2.1+** - Encryption tool for secrets (optional)
|
||||||
|
|
||||||
### Adding to PATH
|
### Adding to PATH
|
||||||
|
|
||||||
To use the CLI globally, add it to your PATH:
|
Recommended installation uses a symlink plus the `prvng` shell alias:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create symbolic link
|
# Symlink the bash wrapper into ~/.local/bin
|
||||||
ln -sf "$(pwd)/provisioning/core/cli/provisioning" /usr/local/bin/provisioning
|
ln -sf "$(pwd)/provisioning/core/cli/provisioning" "$HOME/.local/bin/provisioning"
|
||||||
|
|
||||||
# Or add to PATH in your shell config (~/.bashrc, ~/.zshrc, etc.)
|
# Optional shell alias (add to ~/.bashrc / ~/.zshrc)
|
||||||
export PATH="$PATH:/path/to/project-provisioning/provisioning/core/cli"
|
alias prvng='provisioning'
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify installation:
|
Verify installation:
|
||||||
|
|
@ -77,6 +89,7 @@ Verify installation:
|
||||||
```text
|
```text
|
||||||
provisioning version
|
provisioning version
|
||||||
provisioning help
|
provisioning help
|
||||||
|
prvng s list # alias + single-char shortcut
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
@ -120,6 +133,34 @@ provisioning cluster create my-cluster
|
||||||
provisioning server ssh hostname-01
|
provisioning server ssh hostname-01
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### DAG, Components & Workflows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inspect workspace DAG composition (nodes, edges, health gates)
|
||||||
|
provisioning dag show --infra wuji
|
||||||
|
provisioning dag validate --infra wuji
|
||||||
|
provisioning dag export --infra wuji --format dot
|
||||||
|
|
||||||
|
# Components and extension capabilities
|
||||||
|
provisioning component list
|
||||||
|
provisioning component info postgresql
|
||||||
|
provisioning extensions capabilities
|
||||||
|
provisioning extensions graph
|
||||||
|
|
||||||
|
# Workflows (topological scheduling + NATS events)
|
||||||
|
provisioning workflow list
|
||||||
|
provisioning workflow run deploy-services --infra libre-daoshi
|
||||||
|
provisioning workflow status <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Registry & Fast Path
|
||||||
|
|
||||||
|
Every `prvng`/`provisioning` invocation validates the command against a JSON cache
|
||||||
|
rebuilt from `nulib/commands-registry.ncl` whenever the source is newer. Single-char
|
||||||
|
aliases (`s`, `t`, `c`, `e`, `w`, `j`, `b`, `o`, `a`) are expanded in the bash wrapper
|
||||||
|
before dispatch. Adding a new top-level command requires a registry entry **plus** a
|
||||||
|
dispatch case in `cli/provisioning` — see `nulib/main_provisioning/ADDING_COMMANDS.md`.
|
||||||
|
|
||||||
### Quick Reference
|
### Quick Reference
|
||||||
|
|
||||||
For fastest command reference:
|
For fastest command reference:
|
||||||
|
|
@ -363,7 +404,7 @@ The project follows a three-phase migration:
|
||||||
|
|
||||||
### Required
|
### Required
|
||||||
|
|
||||||
- **Nushell 0.109.0+** - Shell and scripting language
|
- **Nushell 0.112.2** - Shell and scripting language
|
||||||
- **Nickel 1.15.1+** - Configuration language
|
- **Nickel 1.15.1+** - Configuration language
|
||||||
|
|
||||||
### Recommended
|
### Recommended
|
||||||
|
|
@ -491,14 +532,35 @@ See project root LICENSE file.
|
||||||
|
|
||||||
## Recent Updates
|
## Recent Updates
|
||||||
|
|
||||||
|
### 2026-04-17 - DAG architecture, commands-registry, Nushell 0.110/0.112 refactor
|
||||||
|
|
||||||
|
- **Unified Component Architecture**: `components/`, `workflow.nu`, and `components.nu`
|
||||||
|
implement the libre-daoshi unified model (ComponentDef, WorkflowDef, capabilities).
|
||||||
|
See `memory/unified_component_arch.md` and ADRs 020/021.
|
||||||
|
- **Three-layer DAG**: `dag.nu` + `lib_provisioning/config/loader/dag.nu` add
|
||||||
|
`dag show/validate/export` backed by `schemas/lib/dag/*.ncl`; orchestrator emits
|
||||||
|
NATS events via `WorkspaceComposition::into_workflow`.
|
||||||
|
- **Commands-registry cache**: `nulib/commands-registry.ncl` feeds the bash wrapper's
|
||||||
|
`_validate_command`; fast-path single-char alias expansion avoids cold Nu startup.
|
||||||
|
- **Platform service manager**: new `platform/service-manager.nu`, `platform/startup.nu`,
|
||||||
|
and `bootstrap.nu` consolidate autostart, health checks, and lifecycle.
|
||||||
|
- **Nushell 0.112.2 compliance**: `scripts/refactor-try-catch*.nu` drove the
|
||||||
|
migration — no `try/catch`, no bash redirections, all external commands prefixed.
|
||||||
|
- **TTY stack**: `shlib/*-tty.sh` removed; replaced by `cli/tty-dispatch.sh`,
|
||||||
|
`cli/tty-filter.sh`, and `cli/tty-commands.conf`.
|
||||||
|
- **New domain modules**: `images/` (golden image lifecycle), `workspace/state.nu` +
|
||||||
|
`workspace/sync.nu`, `main_provisioning/ontoref-queries.nu`, `main_provisioning/fip.nu`,
|
||||||
|
`main_provisioning/state.nu`, `main_provisioning/extensions.nu`.
|
||||||
|
- **Config loader overhaul**: `loader/core.nu` slimmed (−759 lines of legacy paths),
|
||||||
|
`cache/core.nu` refactored, `loaders/file_loader.nu` removed.
|
||||||
|
|
||||||
### 2026-01-14 - Terminology Migration & i18n
|
### 2026-01-14 - Terminology Migration & i18n
|
||||||
- **Cluster → Taskserv**: Complete refactoring of cluster references to taskserv throughout nulib/ modules
|
|
||||||
- **Fluent i18n System**: Internationalization framework with automatic locale detection
|
- **Cluster → Taskserv**: Complete refactoring across nulib/ modules
|
||||||
|
- **Fluent i18n System**: Automatic locale detection with en-US fallback
|
||||||
- Enhanced ANSI output formatting for improved CLI readability
|
- Enhanced ANSI output formatting for improved CLI readability
|
||||||
- Updated handlers, utilities, and discovery modules for consistency
|
|
||||||
- Locale support: en-US (default) with framework for es-ES, fr-FR, de-DE, etc.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Maintained By**: Core Team
|
**Maintained By**: Core Team
|
||||||
**Last Updated**: 2026-01-14
|
**Last Updated**: 2026-04-17
|
||||||
|
|
|
||||||
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
|
||||||
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
|
||||||
1573
cli/provisioning
1573
cli/provisioning
File diff suppressed because it is too large
Load diff
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
|
||||||
|
|
@ -32,11 +32,26 @@ def install_from_library [
|
||||||
$"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
|
$"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
|
||||||
$"(_ansi purple_bold)from library(_ansi reset)"
|
$"(_ansi purple_bold)from library(_ansi reset)"
|
||||||
)
|
)
|
||||||
let taskservs_path = (get-taskservs-path)
|
let base = (get-taskservs-path)
|
||||||
( run_taskserv $defs
|
let name = $defs.taskserv.name
|
||||||
($taskservs_path | path join $defs.taskserv.name | path join $defs.taskserv_profile)
|
# Resolve the script directory with profile → mode fallback chain:
|
||||||
($wk_server | path join $defs.taskserv.name)
|
# 1. Exact profile name (e.g. "k0sctl")
|
||||||
)
|
# 2. "taskserv" (canonical mode dir — was "default/" pre-migration)
|
||||||
|
# 3. Error with actionable message
|
||||||
|
let profile = $defs.taskserv_profile
|
||||||
|
let by_profile = ($base | path join $name | path join $profile)
|
||||||
|
let by_taskserv = ($base | path join $name | path join "taskserv")
|
||||||
|
let lib_path = if ($by_profile | path exists) {
|
||||||
|
$by_profile
|
||||||
|
} else if ($by_taskserv | path exists) {
|
||||||
|
if $profile != "default" {
|
||||||
|
_print $"(_ansi yellow)⚠ profile '($profile)' not found for ($name), falling back to taskserv/(_ansi reset)"
|
||||||
|
}
|
||||||
|
$by_taskserv
|
||||||
|
} else {
|
||||||
|
error make { msg: $"No script directory for component '($name)': tried ($by_profile) and ($by_taskserv)" }
|
||||||
|
}
|
||||||
|
( run_taskserv $defs $lib_path ($wk_server | path join $name) )
|
||||||
}
|
}
|
||||||
|
|
||||||
export def on_taskservs [
|
export def on_taskservs [
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export def provisioning_options [
|
||||||
source: string
|
source: string
|
||||||
] {
|
] {
|
||||||
let provisioning_name = (get-provisioning-name)
|
let provisioning_name = (get-provisioning-name)
|
||||||
let provisioning_path = (get-base-path)
|
let provisioning_path = (get-config-base-path)
|
||||||
let provisioning_url = (get-provisioning-url)
|
let provisioning_url = (get-provisioning-url)
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,31 @@ export def run_taskserv_library [
|
||||||
let nickel_temp = ($taskserv_env_path | path join "nickel"| path join (mktemp --tmpdir-path $taskserv_env_path --suffix ".ncl" | path basename))
|
let nickel_temp = ($taskserv_env_path | path join "nickel"| path join (mktemp --tmpdir-path $taskserv_env_path --suffix ".ncl" | path basename))
|
||||||
|
|
||||||
let wk_format = if (get-provisioning-wk-format) == "json" { "json" } else { "yaml" }
|
let wk_format = if (get-provisioning-wk-format) == "json" { "json" } else { "yaml" }
|
||||||
|
|
||||||
|
# Resolve floating_ip name → actual IP from provisioning state so taskserv
|
||||||
|
# templates can use server.floating_ip_address without hardcoding.
|
||||||
|
let fip_name = ($defs.server | get -o floating_ip | default "")
|
||||||
|
let resolved_fip_address = if ($fip_name | is-not-empty) {
|
||||||
|
let state_path = ($defs.settings.infra_path | path dirname | path dirname | path join ".provisioning-state.json")
|
||||||
|
if ($state_path | path exists) {
|
||||||
|
let fips = (open $state_path | get -o bootstrap.floating_ips | default {})
|
||||||
|
# FIP names are stored with hyphens converted to underscores as keys
|
||||||
|
# e.g. "librecloud-fip-sgoyol-ingress" → key "sgoyol_ingress" (strip prefix, replace hyphens)
|
||||||
|
let fip_key = ($fip_name | str replace --regex '^librecloud-fip-' '' | str replace --all '-' '_')
|
||||||
|
$fips | get -o $fip_key | default {} | get -o ip | default ""
|
||||||
|
} else { "" }
|
||||||
|
} else { "" }
|
||||||
|
|
||||||
|
let server_ctx = if ($resolved_fip_address | is-not-empty) {
|
||||||
|
$defs.server | merge { floating_ip_address: $resolved_fip_address }
|
||||||
|
} else {
|
||||||
|
$defs.server
|
||||||
|
}
|
||||||
|
|
||||||
let wk_data = { # providers: $defs.settings.providers,
|
let wk_data = { # providers: $defs.settings.providers,
|
||||||
defs: $defs.settings.data,
|
defs: $defs.settings.data,
|
||||||
pos: $defs.pos,
|
pos: $defs.pos,
|
||||||
server: $defs.server
|
server: $server_ctx
|
||||||
}
|
}
|
||||||
if $wk_format == "json" {
|
if $wk_format == "json" {
|
||||||
$wk_data | to json | save --force $wk_vars
|
$wk_data | to json | save --force $wk_vars
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@ export def format_timestamp [timestamp: int]: nothing -> string {
|
||||||
|
|
||||||
# Retry function with exponential backoff (no try-catch)
|
# Retry function with exponential backoff (no try-catch)
|
||||||
export def retry_with_backoff [closure: closure, max_attempts: int = 3, initial_delay: int = 1]: nothing -> any {
|
export def retry_with_backoff [closure: closure, max_attempts: int = 3, initial_delay: int = 1]: nothing -> any {
|
||||||
let mut attempts = 0
|
mut attempts = 0
|
||||||
let mut delay = $initial_delay
|
mut delay = $initial_delay
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let result = (do { $closure | call } | complete)
|
let result = (do { $closure | call } | complete)
|
||||||
|
|
|
||||||
314
nulib/commands-registry.ncl
Normal file
314
nulib/commands-registry.ncl
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
# Command Registry Default Values
|
||||||
|
|
||||||
|
let { make_command, .. } = import "schemas/commands_registry/defaults.ncl" in
|
||||||
|
let cmd_reg_schema = import "schemas/commands_registry/schema.ncl" in
|
||||||
|
|
||||||
|
{
|
||||||
|
commands = [
|
||||||
|
make_command {
|
||||||
|
command = "help",
|
||||||
|
aliases = ["h", "-h", "--help"],
|
||||||
|
uses_cache = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Show help for commands",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "platform",
|
||||||
|
aliases = ["plat", "p"],
|
||||||
|
uses_cache = true,
|
||||||
|
help_category = "platform",
|
||||||
|
description = "Manage platform services",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "guide",
|
||||||
|
aliases = ["guides", "howto"],
|
||||||
|
uses_cache = true,
|
||||||
|
help_category = "guides",
|
||||||
|
description = "Show guides and tutorials",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "shortcuts",
|
||||||
|
aliases = ["sc"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "guides",
|
||||||
|
description = "Show command shortcuts",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "quickstart",
|
||||||
|
aliases = ["quick"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "guides",
|
||||||
|
description = "Quick start guide",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "from-scratch",
|
||||||
|
aliases = ["scratch"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "guides",
|
||||||
|
description = "Start from scratch guide",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "customize",
|
||||||
|
aliases = ["custom"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "guides",
|
||||||
|
description = "Customization guide",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "bootstrap",
|
||||||
|
aliases = ["bstrap"],
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "L1 Hetzner resource bootstrap (network, firewall, SSH key, Floating IPs)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "fip",
|
||||||
|
aliases = ["floating-ip"],
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Floating IP management (list, show, assign, unassign, protection)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "volume",
|
||||||
|
aliases = ["vol"],
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Volume management (list, create, attach, detach, delete)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "server",
|
||||||
|
aliases = ["s"],
|
||||||
|
requires_daemon = true,
|
||||||
|
requires_services = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Server management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "ssh",
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "SSH shortcut: connect to a server by hostname (e.g. prvng ssh sgoyol-1)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "taskserv",
|
||||||
|
aliases = ["task", "t"],
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Task server management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "component",
|
||||||
|
aliases = ["c", "comp", "cl"],
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Component management — list, show, and status for workspace component instances",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "extension",
|
||||||
|
aliases = ["e", "ext"],
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Extension catalog — browse extensions/components/ definitions and metadata",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "create",
|
||||||
|
aliases = ["new"],
|
||||||
|
requires_args = true,
|
||||||
|
requires_daemon = true,
|
||||||
|
requires_services = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Create resources (server, taskserv, cluster)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "delete",
|
||||||
|
aliases = ["d"],
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "infrastructure",
|
||||||
|
description = "Delete resources (server, taskserv, cluster)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "workspace",
|
||||||
|
aliases = ["ws"],
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "workspace",
|
||||||
|
description = "Workspace management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "validate",
|
||||||
|
aliases = ["val"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "config",
|
||||||
|
description = "Validate configuration",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "config",
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "setup",
|
||||||
|
description = "Configuration management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "env",
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "config",
|
||||||
|
description = "Environment configuration",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "alias",
|
||||||
|
aliases = ["a", "al"],
|
||||||
|
uses_cache = true,
|
||||||
|
help_category = "utils",
|
||||||
|
description = "Show command aliases — alias list (al) displays the full shortcut table",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "show",
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "config",
|
||||||
|
description = "Show configuration",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "setup",
|
||||||
|
aliases = ["st"],
|
||||||
|
uses_cache = true,
|
||||||
|
help_category = "setup",
|
||||||
|
description = "Initial setup",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "state",
|
||||||
|
aliases = ["st"],
|
||||||
|
uses_cache = false,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "state",
|
||||||
|
description = "Workspace provisioning state management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "job",
|
||||||
|
aliases = ["j"],
|
||||||
|
requires_args = true,
|
||||||
|
uses_cache = false,
|
||||||
|
help_category = "orchestration",
|
||||||
|
description = "Orchestrator job management (list, status, monitor, submit)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "workflow",
|
||||||
|
aliases = ["w", "wflow"],
|
||||||
|
requires_args = true,
|
||||||
|
uses_cache = false,
|
||||||
|
help_category = "orchestration",
|
||||||
|
description = "Workspace workflow management — WorkflowDef lifecycle (list, show, run, validate, status)",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "batch",
|
||||||
|
aliases = ["b", "bat"],
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "orchestration",
|
||||||
|
description = "Batch operations",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "orchestrator",
|
||||||
|
aliases = ["o", "orch"],
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "orchestration",
|
||||||
|
description = "Orchestrator management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "module",
|
||||||
|
aliases = ["mod"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "development",
|
||||||
|
description = "Module management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "layer",
|
||||||
|
aliases = ["lyr"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "development",
|
||||||
|
description = "Layer management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "discover",
|
||||||
|
aliases = ["disc"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "development",
|
||||||
|
description = "Discover modules",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "status",
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "diagnostics",
|
||||||
|
description = "Show status",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "health",
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "diagnostics",
|
||||||
|
description = "Health check",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "diagnostics",
|
||||||
|
aliases = ["diag"],
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "diagnostics",
|
||||||
|
description = "Run diagnostics",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "build",
|
||||||
|
aliases = ["bd"],
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "build",
|
||||||
|
description = "Build operations",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "auth",
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "authentication",
|
||||||
|
description = "Authentication management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "login",
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "authentication",
|
||||||
|
description = "Login",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "integrations",
|
||||||
|
aliases = ["int"],
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "integrations",
|
||||||
|
description = "Integration management",
|
||||||
|
},
|
||||||
|
make_command {
|
||||||
|
command = "vm",
|
||||||
|
requires_daemon = true,
|
||||||
|
uses_cache = true,
|
||||||
|
requires_args = true,
|
||||||
|
help_category = "vm",
|
||||||
|
description = "VM management",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
312
nulib/components/mod.nu
Normal file
312
nulib/components/mod.nu
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
#!/usr/bin/env nu
|
||||||
|
# Component management module — list, show, status for extensions/components.
|
||||||
|
#
|
||||||
|
# Two perspectives per component:
|
||||||
|
# extension — what exists in extensions/components/{name}/ (metadata, modes, contract)
|
||||||
|
# workspace — how it's instantiated in infra/{ws}/components/{name}.ncl
|
||||||
|
#
|
||||||
|
# Ontology data (FSM state, edges) is read via ontoref when available (defensive).
|
||||||
|
|
||||||
|
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft, default-ncl-paths]
|
||||||
|
|
||||||
|
# Resolve the extensions/components/ base path.
|
||||||
|
def _comp-ext-base []: nothing -> string {
|
||||||
|
let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "")
|
||||||
|
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
if ($prov | is-not-empty) {
|
||||||
|
let p = ($prov | path join "extensions" | path join "components")
|
||||||
|
if ($p | path exists) { return $p }
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve the workspace root for a given workspace name.
|
||||||
|
# Checks PROVISIONING_KLOUD_PATH env, then walks known workspace directories.
|
||||||
|
def _ws-root [workspace: string]: nothing -> string {
|
||||||
|
if ($workspace | is-empty) { return "" }
|
||||||
|
let from_env = ($env.PROVISIONING_KLOUD_PATH? | default "")
|
||||||
|
if ($from_env | is-not-empty) and ($from_env | path basename) == $workspace {
|
||||||
|
return $from_env
|
||||||
|
}
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
if ($prov | is-not-empty) {
|
||||||
|
let ws_root = ($prov | path dirname | path join "workspaces" | path join $workspace)
|
||||||
|
if ($ws_root | path exists) { return $ws_root }
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export a Nickel file to a record. Returns null on failure.
|
||||||
|
# Uses default-ncl-paths to match the daemon's cache key derivation.
|
||||||
|
def _ncl-export [file_path: string]: nothing -> any {
|
||||||
|
let ws_root = ($file_path | path dirname | path dirname | path dirname)
|
||||||
|
ncl-eval-soft $file_path (default-ncl-paths $ws_root) null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read FSM dimension for a component from state.ncl via ontoref or raw NCL export.
|
||||||
|
def _read-fsm-state [name: string, ws_root: string]: nothing -> record {
|
||||||
|
let dim_id = $"($name)-status"
|
||||||
|
# Try ontoref first (richer output)
|
||||||
|
let onto_result = (do {
|
||||||
|
^ontoref describe state $dim_id --fmt json --workspace $ws_root
|
||||||
|
} | complete)
|
||||||
|
if $onto_result.exit_code == 0 {
|
||||||
|
let parsed = (do { $onto_result.stdout | from json } | complete)
|
||||||
|
if $parsed.exit_code == 0 { return $parsed.stdout }
|
||||||
|
}
|
||||||
|
# Fallback: export state.ncl and filter
|
||||||
|
let state_path = ($ws_root | path join ".ontology" | path join "state.ncl")
|
||||||
|
if not ($state_path | path exists) { return {} }
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
let state_data = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) {})
|
||||||
|
if ($state_data | is-empty) { return {} }
|
||||||
|
let dims = ($state_data | get -o dimensions | default [])
|
||||||
|
$dims | where {|d| ($d | get -o id | default "") == $dim_id } | get 0? | default {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read ontology node and edges for a component from core.ncl.
|
||||||
|
def _read-onto-node [name: string, ws_root: string]: nothing -> record {
|
||||||
|
let core_path = ($ws_root | path join ".ontology" | path join "core.ncl")
|
||||||
|
if not ($core_path | path exists) { return { node: null, edges_from: [], edges_to: [] } }
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
let data = (ncl-eval-soft $core_path (default-ncl-paths $ws_root) null)
|
||||||
|
if $data == null { return { node: null, edges_from: [], edges_to: [] } }
|
||||||
|
let nodes = ($data | get -o nodes | default [])
|
||||||
|
let edges = ($data | get -o edges | default [])
|
||||||
|
let node = ($nodes | where {|n| ($n | get -o id | default "") == $name } | get 0? | default null)
|
||||||
|
let edges_from = ($edges | where {|e| ($e | get -o from | default "") == $name })
|
||||||
|
let edges_to = ($edges | where {|e| ($e | get -o to | default "") == $name })
|
||||||
|
{ node: $node, edges_from: $edges_from, edges_to: $edges_to }
|
||||||
|
}
|
||||||
|
|
||||||
|
# List all components from extensions/components/ with optional mode filter and workspace state.
|
||||||
|
export def component-list [mode: string, workspace: string]: nothing -> nothing {
|
||||||
|
let base = (_comp-ext-base)
|
||||||
|
if ($base | is-empty) or not ($base | path exists) {
|
||||||
|
print "❌ extensions/components/ not found. Set PROVISIONING env var."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws_root = (_ws-root $workspace)
|
||||||
|
let show_state = ($ws_root | is-not-empty)
|
||||||
|
|
||||||
|
mut rows = []
|
||||||
|
for item in (ls $base | where type == "dir") {
|
||||||
|
let name = ($item.name | path basename)
|
||||||
|
let meta_p = ($item.name | path join "metadata.ncl")
|
||||||
|
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
|
||||||
|
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
|
||||||
|
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
|
||||||
|
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
|
||||||
|
|
||||||
|
# Mode filter
|
||||||
|
if ($mode | is-not-empty) and ($mode not-in $modes) { continue }
|
||||||
|
|
||||||
|
let state = if $show_state {
|
||||||
|
let dim = (_read-fsm-state $name $ws_root)
|
||||||
|
if ($dim | is-empty) { "—" } else {
|
||||||
|
let cur = ($dim | get -o current_state | default "—")
|
||||||
|
let des = ($dim | get -o desired_state | default "")
|
||||||
|
if ($des | is-not-empty) and $cur != $des { $"($cur) → ($des)" } else { $cur }
|
||||||
|
}
|
||||||
|
} else { "—" }
|
||||||
|
|
||||||
|
$rows = ($rows | append {
|
||||||
|
name: $name
|
||||||
|
mode: ($modes | str join "·")
|
||||||
|
state: $state
|
||||||
|
version: $version
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rows | is-empty) {
|
||||||
|
print "No components found."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = if $show_state { $"Components [workspace: ($workspace)]" } else { "Components [extension catalog]" }
|
||||||
|
print $header
|
||||||
|
print "────────────────────────────────────────────────────────────"
|
||||||
|
$rows | table
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show full details for a named component.
|
||||||
|
export def component-show [name: string, workspace: string, ext_only: bool]: nothing -> nothing {
|
||||||
|
let base = (_comp-ext-base)
|
||||||
|
let ext_dir = ($base | path join $name)
|
||||||
|
if not ($ext_dir | path exists) {
|
||||||
|
print $"❌ Component '($name)' not found in extensions/components/"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta_p = ($ext_dir | path join "metadata.ncl")
|
||||||
|
let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null }
|
||||||
|
|
||||||
|
# Extension section
|
||||||
|
let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] }
|
||||||
|
let version = if $meta != null { $meta | get -o version | default "" } else { "" }
|
||||||
|
let desc = if $meta != null { $meta | get -o description | default "" } else { "" }
|
||||||
|
let tags = if $meta != null { $meta | get -o tags | default [] | str join " · " } else { "" }
|
||||||
|
|
||||||
|
# Defaults (requires/provides/operations from nickel/defaults.ncl)
|
||||||
|
let defaults_p = ($ext_dir | path join "nickel" | path join "defaults.ncl")
|
||||||
|
let defaults = if ($defaults_p | path exists) { _ncl-export $defaults_p } else { null }
|
||||||
|
let def_rec = if $defaults != null { $defaults | get -o $name | default {} } else { {} }
|
||||||
|
let requires = ($def_rec | get -o requires | default {})
|
||||||
|
let provides = ($def_rec | get -o provides | default {})
|
||||||
|
let operations = ($def_rec | get -o operations | default {})
|
||||||
|
|
||||||
|
print $"┌─ ($name | str upcase) ─────────────────────────────────"
|
||||||
|
print $"│ ($desc)"
|
||||||
|
print $"├────────────────────────────────────────────────────────"
|
||||||
|
let modes_str = ($modes | str join " · ")
|
||||||
|
print $"│ VERSION ($version)"
|
||||||
|
print $"│ MODES ($modes_str)"
|
||||||
|
if ($tags | is-not-empty) { print $"│ TAGS ($tags)" }
|
||||||
|
|
||||||
|
# REQUIRES
|
||||||
|
let req_storage = ($requires | get -o storage | default null)
|
||||||
|
let req_ports = ($requires | get -o ports | default [])
|
||||||
|
let req_creds = ($requires | get -o credentials | default [])
|
||||||
|
if $req_storage != null or ($req_ports | is-not-empty) or ($req_creds | is-not-empty) {
|
||||||
|
print "├─── REQUIRES ───────────────────────────────────────────"
|
||||||
|
if $req_storage != null {
|
||||||
|
let persist_label = if ($req_storage.persistent? | default false) { "persistent" } else { "ephemeral" }
|
||||||
|
let stor_size = ($req_storage.size? | default "?")
|
||||||
|
print $"│ storage ($stor_size) ($persist_label)"
|
||||||
|
}
|
||||||
|
for p in $req_ports {
|
||||||
|
let pport = ($p.port? | default 0 | into string)
|
||||||
|
let pproto = ($p.protocol? | default "TCP")
|
||||||
|
let pexpose = ($p.exposure? | default "internal")
|
||||||
|
print $"│ port ($pport)/($pproto) \(($pexpose)\)"
|
||||||
|
}
|
||||||
|
if ($req_creds | is-not-empty) {
|
||||||
|
let creds_str = ($req_creds | str join " · ")
|
||||||
|
print $"│ creds ($creds_str)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# PROVIDES
|
||||||
|
let prov_svc = ($provides | get -o service | default "")
|
||||||
|
let prov_port = ($provides | get -o port | default null)
|
||||||
|
let prov_dbs = ($provides | get -o databases | default [])
|
||||||
|
if ($prov_svc | is-not-empty) or $prov_port != null or ($prov_dbs | is-not-empty) {
|
||||||
|
print "├─── PROVIDES ───────────────────────────────────────────"
|
||||||
|
if ($prov_svc | is-not-empty) and $prov_port != null {
|
||||||
|
print $"│ service ($prov_svc):($prov_port)"
|
||||||
|
} else if ($prov_svc | is-not-empty) {
|
||||||
|
print $"│ service ($prov_svc)"
|
||||||
|
}
|
||||||
|
if ($prov_dbs | is-not-empty) {
|
||||||
|
let dbs_str = ($prov_dbs | str join " · ")
|
||||||
|
print $"│ databases ($dbs_str)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# OPERATIONS
|
||||||
|
let ops_enabled = ($operations | transpose k v | where v == true | each {|r| $r.k })
|
||||||
|
if ($ops_enabled | is-not-empty) {
|
||||||
|
let ops_str = ($ops_enabled | str join " · ")
|
||||||
|
print "├─── OPERATIONS ─────────────────────────────────────────"
|
||||||
|
print $"│ ($ops_str)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $ext_only and ($workspace | is-not-empty) {
|
||||||
|
let ws_root = (_ws-root $workspace)
|
||||||
|
if ($ws_root | is-not-empty) {
|
||||||
|
# Workspace instance
|
||||||
|
let comp_p = ($ws_root | path join "infra" | path join $workspace | path join "components" | path join $"($name).ncl")
|
||||||
|
let comp_data = if ($comp_p | path exists) { _ncl-export $comp_p } else { null }
|
||||||
|
let inst = if $comp_data != null { $comp_data | get -o $name | default {} } else { {} }
|
||||||
|
let inst_mode = ($inst | get -o mode | default "")
|
||||||
|
let inst_ns = ($inst | get -o namespace | default "")
|
||||||
|
let inst_tgt = ($inst | get -o target | default "")
|
||||||
|
|
||||||
|
print "├─── WORKSPACE INSTANCE ─────────────────────────────────"
|
||||||
|
if ($inst_mode | is-not-empty) { print $"│ mode ($inst_mode)" }
|
||||||
|
if ($inst_ns | is-not-empty) { print $"│ namespace ($inst_ns)" }
|
||||||
|
if ($inst_tgt | is-not-empty) { print $"│ target ($inst_tgt)" }
|
||||||
|
|
||||||
|
# FSM state
|
||||||
|
let dim = (_read-fsm-state $name $ws_root)
|
||||||
|
if not ($dim | is-empty) {
|
||||||
|
let cur = ($dim | get -o current_state | default "—")
|
||||||
|
let des = ($dim | get -o desired_state | default "")
|
||||||
|
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
|
||||||
|
let blk_short = ($blk | str substring 0..80)
|
||||||
|
print "├─── STATE ───────────────────────────────────────────"
|
||||||
|
print $"│ current ($cur)"
|
||||||
|
if ($des | is-not-empty) { print $"│ desired ($des)" }
|
||||||
|
if ($blk | is-not-empty) { print $"│ blocker ($blk_short)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ontology
|
||||||
|
let onto = (_read-onto-node $name $ws_root)
|
||||||
|
if $onto.node != null {
|
||||||
|
let node = $onto.node
|
||||||
|
let node_lvl = ($node.level? | default "?")
|
||||||
|
let node_pole = ($node.pole? | default "?")
|
||||||
|
print "├─── ONTOLOGY ────────────────────────────────────────"
|
||||||
|
print $"│ node ($name) \(($node_lvl) / ($node_pole)\)"
|
||||||
|
let arts = ($node | get -o artifact_paths | default [])
|
||||||
|
if ($arts | is-not-empty) {
|
||||||
|
let arts_str = ($arts | str join " · ")
|
||||||
|
print $"│ artifacts ($arts_str)"
|
||||||
|
}
|
||||||
|
let adrs = ($node | get -o adrs | default [])
|
||||||
|
if ($adrs | is-not-empty) {
|
||||||
|
let adrs_str = ($adrs | str join " · ")
|
||||||
|
print $"│ adrs ($adrs_str)"
|
||||||
|
}
|
||||||
|
if ($onto.edges_from | is-not-empty) {
|
||||||
|
let consumers = ($onto.edges_from | each {|e|
|
||||||
|
let eto = ($e | get -o to | default "?")
|
||||||
|
let ekind = ($e | get -o kind | default "")
|
||||||
|
$"($eto) \(($ekind)\)"
|
||||||
|
} | str join " · ")
|
||||||
|
print $"│ used-by ($consumers)"
|
||||||
|
}
|
||||||
|
if ($onto.edges_to | is-not-empty) {
|
||||||
|
let uses = ($onto.edges_to | each {|e|
|
||||||
|
let efrom = ($e | get -o from | default "?")
|
||||||
|
let ekind = ($e | get -o kind | default "")
|
||||||
|
$"($efrom) \(($ekind)\)"
|
||||||
|
} | str join " · ")
|
||||||
|
print $"│ uses ($uses)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print "└────────────────────────────────────────────────────────"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show only FSM state for a component.
|
||||||
|
export def component-status [name: string, workspace: string]: nothing -> nothing {
|
||||||
|
if ($workspace | is-empty) {
|
||||||
|
print "❌ --workspace required for status"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let ws_root = (_ws-root $workspace)
|
||||||
|
if ($ws_root | is-empty) {
|
||||||
|
print $"❌ Workspace '($workspace)' not found"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let dim = (_read-fsm-state $name $ws_root)
|
||||||
|
if ($dim | is-empty) {
|
||||||
|
print $"No FSM dimension found for '($name)-status' in ($workspace)"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let cur = ($dim | get -o current_state | default "—")
|
||||||
|
let des = ($dim | get -o desired_state | default "—")
|
||||||
|
let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "")
|
||||||
|
let cat = ($dim | get -o transitions | default [] | get 0? | default {} | get -o catalyst | default "")
|
||||||
|
|
||||||
|
print $"($name) — FSM state [($workspace)]"
|
||||||
|
print $" current: ($cur)"
|
||||||
|
print $" desired: ($des)"
|
||||||
|
if ($blk | is-not-empty) { print $" blocker: ($blk)" }
|
||||||
|
if ($cat | is-not-empty) { print $" catalyst: ($cat)" }
|
||||||
|
}
|
||||||
15
nulib/env.nu
15
nulib/env.nu
|
|
@ -65,9 +65,16 @@ export-env {
|
||||||
# Just set it to a reasonable default
|
# Just set it to a reasonable default
|
||||||
$env.PROVISIONING_CORE = "/usr/local/provisioning/core"
|
$env.PROVISIONING_CORE = "/usr/local/provisioning/core"
|
||||||
}
|
}
|
||||||
$env.PROVISIONING_PROVIDERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "providers")
|
$env.PROVISIONING_PROVIDERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "providers")
|
||||||
$env.PROVISIONING_TASKSERVS_PATH = ($env.PROVISIONING | path join "extensions" | path join "taskservs")
|
$env.PROVISIONING_COMPONENTS_PATH = ($env.PROVISIONING | path join "extensions" | path join "components")
|
||||||
$env.PROVISIONING_CLUSTERS_PATH = ($env.PROVISIONING | path join "extensions" | path join "clusters")
|
# Keep for backward compat — points to taskservs/ if it exists, falls back to components/
|
||||||
|
let _ts_path = ($env.PROVISIONING | path join "extensions" | 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 "extensions" | path join "clusters")
|
||||||
$env.PROVISIONING_RESOURCES = ($env.PROVISIONING | path join "resources" )
|
$env.PROVISIONING_RESOURCES = ($env.PROVISIONING | path join "resources" )
|
||||||
$env.PROVISIONING_NOTIFY_ICON = ($env.PROVISIONING_RESOURCES | path join "images"| path join "cloudnative.png")
|
$env.PROVISIONING_NOTIFY_ICON = ($env.PROVISIONING_RESOURCES | path join "images"| path join "cloudnative.png")
|
||||||
|
|
||||||
|
|
@ -124,7 +131,6 @@ export-env {
|
||||||
|
|
||||||
$env.PROVISIONING_KEYS_PATH = (config-get "paths.files.keys" ".keys.ncl" --config $config)
|
$env.PROVISIONING_KEYS_PATH = (config-get "paths.files.keys" ".keys.ncl" --config $config)
|
||||||
|
|
||||||
$env.PROVISIONING_USE_nickel = if (^bash -c "type -P nickel" | is-not-empty) { true } else { false }
|
|
||||||
$env.PROVISIONING_USE_NICKEL_PLUGIN = if ( (version).installed_plugins | str contains "nickel" ) { true } else { false }
|
$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 = ($env.PROVISIONING_$TOOLS_PATH | path join "parsetemplate.py")
|
||||||
#$env.PROVISIONING_J2_PARSER = (^bash -c "type -P tera")
|
#$env.PROVISIONING_J2_PARSER = (^bash -c "type -P tera")
|
||||||
|
|
@ -211,6 +217,7 @@ export-env {
|
||||||
# Nickel Module Path Configuration
|
# Nickel Module Path Configuration
|
||||||
# Set up NICKEL_IMPORT_PATH to help Nickel resolve modules when running from different directories
|
# 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.NICKEL_IMPORT_PATH = ($env.NICKEL_IMPORT_PATH? | default [] | append [
|
||||||
|
$env.PROVISIONING
|
||||||
($env.PROVISIONING | path join "nickel")
|
($env.PROVISIONING | path join "nickel")
|
||||||
($env.PROVISIONING_PROVIDERS_PATH)
|
($env.PROVISIONING_PROVIDERS_PATH)
|
||||||
$env.PWD
|
$env.PWD
|
||||||
|
|
|
||||||
|
|
@ -156,13 +156,14 @@ def provisioning-help [category?: string = ""] {
|
||||||
"concepts" | "concept" => "concepts"
|
"concepts" | "concept" => "concepts"
|
||||||
"guides" | "guide" | "howto" => "guides"
|
"guides" | "guide" | "howto" => "guides"
|
||||||
"integrations" | "integration" | "int" => "integrations"
|
"integrations" | "integration" | "int" => "integrations"
|
||||||
|
"build" | "bi" | "build-image" => "build"
|
||||||
_ => "unknown"
|
_ => "unknown"
|
||||||
})
|
})
|
||||||
|
|
||||||
if $result == "unknown" {
|
if $result == "unknown" {
|
||||||
print $"❌ Unknown help category: \"($category)\"\n"
|
print $"❌ Unknown help category: \"($category)\"\n"
|
||||||
print "Available help categories: infrastructure, orchestration, development, workspace, setup, platform,"
|
print "Available help categories: infrastructure, orchestration, development, workspace, setup, platform,"
|
||||||
print "authentication, mfa, plugins, utilities, tools, vm, diagnostics, concepts, guides, integrations"
|
print "authentication, mfa, plugins, utilities, tools, vm, diagnostics, concepts, guides, integrations, build"
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +184,7 @@ def provisioning-help [category?: string = ""] {
|
||||||
"concepts" => (help-concepts)
|
"concepts" => (help-concepts)
|
||||||
"guides" => (help-guides)
|
"guides" => (help-guides)
|
||||||
"integrations" => (help-integrations)
|
"integrations" => (help-integrations)
|
||||||
|
"build" => (help-build)
|
||||||
_ => (help-main)
|
_ => (help-main)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,6 +240,7 @@ def help-main [] {
|
||||||
["💡", "concepts", "", $concepts_desc],
|
["💡", "concepts", "", $concepts_desc],
|
||||||
["📖", "guides", "[guide]", $guides_desc],
|
["📖", "guides", "[guide]", $guides_desc],
|
||||||
["🌐", "integrations", "[int]", $int_desc],
|
["🌐", "integrations", "[int]", $int_desc],
|
||||||
|
["📦", "build", "[bi]", "Role image build, state, and watch"],
|
||||||
]
|
]
|
||||||
|
|
||||||
let categories_table = (format-categories $rows)
|
let categories_table = (format-categories $rows)
|
||||||
|
|
@ -439,13 +442,56 @@ def help-workspace [] {
|
||||||
|
|
||||||
# Platform help
|
# Platform help
|
||||||
def help-platform [] {
|
def help-platform [] {
|
||||||
let title = (get-help-string "help-platform-title")
|
|
||||||
let intro = (get-help-string "help-platform-intro")
|
|
||||||
let more_info = (get-help-string "help-more-info")
|
|
||||||
(
|
(
|
||||||
(ansi red) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
(ansi red) + (ansi bo) + "🖥️ PLATFORM SERVICES" + (ansi rst) + "\n\n" +
|
||||||
($intro) + "\n\n" +
|
|
||||||
($more_info) + "\n"
|
(ansi green) + (ansi bo) + "[Control Center]" + (ansi rst) + " " + (ansi cyan) + (ansi bo) + "🌐 Web UI + Policy Engine" + (ansi rst) + "\n" +
|
||||||
|
" " + (ansi blue) + "control-center server" + (ansi rst) + "\t\t\t - Start Cedar policy engine " + (ansi cyan) + "--port 8080" + (ansi rst) + "\n" +
|
||||||
|
" " + (ansi blue) + "control-center policy validate" + (ansi rst) + "\t - Validate Cedar policies\n" +
|
||||||
|
" " + (ansi blue) + "control-center policy test" + (ansi rst) + "\t\t - Test policies with data\n" +
|
||||||
|
" " + (ansi blue) + "control-center compliance soc2" + (ansi rst) + "\t - SOC2 compliance check\n" +
|
||||||
|
" " + (ansi blue) + "control-center compliance hipaa" + (ansi rst) + "\t - HIPAA compliance check\n\n" +
|
||||||
|
|
||||||
|
(ansi cyan) + (ansi bo) + " 🎨 Features:" + (ansi rst) + "\n" +
|
||||||
|
" • " + (ansi green) + "Web-based UI" + (ansi rst) + "\t - WASM-powered control center interface\n" +
|
||||||
|
" • " + (ansi green) + "Policy Engine" + (ansi rst) + "\t - Cedar policy evaluation and versioning\n" +
|
||||||
|
" • " + (ansi green) + "Compliance" + (ansi rst) + "\t - SOC2 Type II and HIPAA validation\n" +
|
||||||
|
" • " + (ansi green) + "Security" + (ansi rst) + "\t\t - JWT auth, MFA, RBAC, anomaly detection\n" +
|
||||||
|
" • " + (ansi green) + "Audit Trail" + (ansi rst) + "\t - Complete compliance audit logging\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[Orchestrator]" + (ansi rst) + " Hybrid Rust/Nushell Coordination\n" +
|
||||||
|
" " + (ansi blue) + "orchestrator start" + (ansi rst) + " - Start orchestrator [--background]\n" +
|
||||||
|
" " + (ansi blue) + "orchestrator stop" + (ansi rst) + " - Stop orchestrator\n" +
|
||||||
|
" " + (ansi blue) + "orchestrator status" + (ansi rst) + " - Check if running\n" +
|
||||||
|
" " + (ansi blue) + "orchestrator health" + (ansi rst) + " - Health check with diagnostics\n" +
|
||||||
|
" " + (ansi blue) + "orchestrator logs" + (ansi rst) + " - View logs [--follow]\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[MCP Server]" + (ansi rst) + " AI-Assisted DevOps Integration\n" +
|
||||||
|
" " + (ansi blue) + "mcp-server start" + (ansi rst) + " - Start MCP server [--debug]\n" +
|
||||||
|
" " + (ansi blue) + "mcp-server status" + (ansi rst) + " - Check server status\n\n" +
|
||||||
|
|
||||||
|
(ansi cyan) + (ansi bo) + " 🤖 Features:" + (ansi rst) + "\n" +
|
||||||
|
" • " + (ansi green) + "AI-Powered Parsing" + (ansi rst) + " - Natural language to infrastructure\n" +
|
||||||
|
" • " + (ansi green) + "Multi-Provider" + (ansi rst) + "\t - AWS, UpCloud, Local support\n" +
|
||||||
|
" • " + (ansi green) + "Ultra-Fast" + (ansi rst) + "\t - Microsecond latency, 1000x faster than Python\n" +
|
||||||
|
" • " + (ansi green) + "Type Safe" + (ansi rst) + "\t\t - Compile-time guarantees with zero runtime errors\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "🌐 REST API ENDPOINTS" + (ansi rst) + "\n\n" +
|
||||||
|
(ansi yellow) + "Control Center" + (ansi rst) + " - " + (ansi d) + "http://localhost:8080" + (ansi rst) + "\n" +
|
||||||
|
" • POST /policies/evaluate - Evaluate policy decisions\n" +
|
||||||
|
" • GET /policies - List all policies\n" +
|
||||||
|
" • GET /compliance/soc2 - SOC2 compliance check\n" +
|
||||||
|
" • GET /anomalies - List detected anomalies\n\n" +
|
||||||
|
|
||||||
|
(ansi yellow) + "Orchestrator" + (ansi rst) + " - " + (ansi d) + "http://localhost:8080" + (ansi rst) + "\n" +
|
||||||
|
" • GET /health - Health check\n" +
|
||||||
|
" • GET /tasks - List all tasks\n" +
|
||||||
|
" • POST /workflows/servers/create - Server workflow\n" +
|
||||||
|
" • POST /workflows/batch/submit - Batch workflow\n\n" +
|
||||||
|
|
||||||
|
(ansi d) + "💡 Control Center provides a " + (ansi cyan) + (ansi bo) + "web-based UI" + (ansi rst) + (ansi d) + " for managing policies!\n" +
|
||||||
|
" Access at: " + (ansi cyan) + "http://localhost:8080" + (ansi rst) + (ansi d) + " after starting the server\n" +
|
||||||
|
" Example: provisioning control-center server --port 8080" + (ansi rst) + "\n"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -569,6 +615,49 @@ def help-integrations [] {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build help — role image management
|
||||||
|
def help-build [] {
|
||||||
|
(
|
||||||
|
(ansi yellow) + (ansi bo) + "🏗️ BUILD — Role Image Management" + (ansi rst) + "\n\n" +
|
||||||
|
|
||||||
|
(ansi d) + "Pre-built provider snapshots (nixos-generators → Hetzner snapshot).\n" +
|
||||||
|
"Snapshot IDs and freshness tracked in ~/.config/provisioning/images/.\n" +
|
||||||
|
"Server creation runs a pre-flight check before rendering templates." + (ansi rst) + "\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[Image Lifecycle]" + (ansi rst) + "\n" +
|
||||||
|
" " + (ansi blue) + "build image create <role>" + (ansi rst) + " - Build snapshot for role, save state\n" +
|
||||||
|
" Options: --infra <path> --check --provider <p>\n" +
|
||||||
|
" " + (ansi blue) + "build image list" + (ansi rst) + " - Show all role states (provider, snapshot_id, fresh)\n" +
|
||||||
|
" Options: --provider <p>\n" +
|
||||||
|
" " + (ansi blue) + "build image update <role>" + (ansi rst) + " - Delete stale snapshot and rebuild\n" +
|
||||||
|
" Options: --infra <path> --provider <p> --check\n" +
|
||||||
|
" " + (ansi blue) + "build image delete <role>" + (ansi rst) + " - Remove snapshot from provider + local state\n" +
|
||||||
|
" Options: --provider <p> --yes\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[Monitoring]" + (ansi rst) + "\n" +
|
||||||
|
" " + (ansi blue) + "build image watch" + (ansi rst) + " - Poll freshness of all role images (loop)\n" +
|
||||||
|
" Options: --interval <min> --auto-build --notify-only\n" +
|
||||||
|
" --provider <p> --infra <path>\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[Shortcuts]" + (ansi rst) + "\n" +
|
||||||
|
" " + (ansi d) + "b, build" + (ansi rst) + " → build domain\n" +
|
||||||
|
" " + (ansi d) + "bi, build-image" + (ansi rst) + " → build image\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[Examples]" + (ansi rst) + "\n" +
|
||||||
|
" provisioning build image list\n" +
|
||||||
|
" provisioning build image create cp --infra workspaces/librecloud_hetzner/infra/wuji --check\n" +
|
||||||
|
" provisioning build image create cp --infra workspaces/librecloud_hetzner/infra/wuji\n" +
|
||||||
|
" provisioning build image update worker --infra workspaces/librecloud_hetzner/infra/wuji\n" +
|
||||||
|
" provisioning build image delete storage --yes\n" +
|
||||||
|
" provisioning build image watch --interval 30 --auto-build\n\n" +
|
||||||
|
|
||||||
|
(ansi green) + (ansi bo) + "[State Files]" + (ansi rst) + "\n" +
|
||||||
|
" Location: ~/.config/provisioning/images/<provider>-<role>.ncl\n" +
|
||||||
|
" Schema: provisioning/schemas/infrastructure/images/\n" +
|
||||||
|
" Workspace roles: workspaces/librecloud_hetzner/infra/wuji/images.ncl\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
# Main entry point
|
# Main entry point
|
||||||
def main [...args: string] {
|
def main [...args: string] {
|
||||||
let category = if ($args | length) > 0 { ($args | get 0) } else { "" }
|
let category = if ($args | length) > 0 { ($args | get 0) } else { "" }
|
||||||
|
|
|
||||||
165
nulib/images/create.nu
Normal file
165
nulib/images/create.nu
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
# Image create — render build template, execute, capture snapshot ID, persist state.
|
||||||
|
|
||||||
|
use ./state.nu *
|
||||||
|
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval]
|
||||||
|
|
||||||
|
# Load the ImageRole definition from the workspace images.ncl for a given role name.
|
||||||
|
def load-image-role [infra: string, role: string]: nothing -> record {
|
||||||
|
let images_ncl = ($infra | path join "images.ncl")
|
||||||
|
if not ($images_ncl | path exists) {
|
||||||
|
error make { msg: $"images.ncl not found at ($images_ncl)" }
|
||||||
|
}
|
||||||
|
let data = (ncl-eval $images_ncl [])
|
||||||
|
let roles = ($data | get image_roles? | default {})
|
||||||
|
let role_def = ($roles | get -o $role)
|
||||||
|
if ($role_def | is-empty) {
|
||||||
|
error make { msg: $"Role '($role)' not defined in ($images_ncl)" }
|
||||||
|
}
|
||||||
|
$role_def
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build template context and render via tera plugin.
|
||||||
|
def render-build-template [role_def: record, infra: string, check: bool]: nothing -> string {
|
||||||
|
let tera_loaded = (plugin list | where name == "tera" | length) > 0
|
||||||
|
if not $tera_loaded { plugin use tera }
|
||||||
|
|
||||||
|
let provider = ($role_def | get provider? | default "hetzner")
|
||||||
|
let tpl_name = ($role_def | get template_name? | default "hetzner_build_image.j2")
|
||||||
|
let tpl_path = ($env.PROVISIONING | path join "extensions" | path join "providers"
|
||||||
|
| path join $provider | path join "templates" | path join $tpl_name)
|
||||||
|
|
||||||
|
if not ($tpl_path | path exists) {
|
||||||
|
error make { msg: $"Build template not found: ($tpl_path)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate flake directory: go up 2 levels from infra/wuji to workspace root, then add nixos
|
||||||
|
let infra_expanded = ($infra | path expand)
|
||||||
|
let workspace_root = ($infra_expanded | path dirname | path dirname)
|
||||||
|
let flake_dir = ($workspace_root | path join "nixos")
|
||||||
|
|
||||||
|
let ctx = {
|
||||||
|
image_role: $role_def,
|
||||||
|
ssh_key: ($role_def | get ssh_key? | default ""),
|
||||||
|
location: ($role_def | get location? | default "nbg1"),
|
||||||
|
flake_dir: $flake_dir,
|
||||||
|
now: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
provisioning_version: ($env.PROVISIONING_VERSION? | default "0.0.0"),
|
||||||
|
check: $check,
|
||||||
|
}
|
||||||
|
$ctx | tera-render $tpl_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse the SNAPSHOT_ID=<id> line from build script stdout.
|
||||||
|
def extract-snapshot-id [output: string]: nothing -> string {
|
||||||
|
let line = ($output | lines | find "SNAPSHOT_ID=" | first?)
|
||||||
|
if ($line | is-empty) {
|
||||||
|
error make { msg: "Build script did not emit SNAPSHOT_ID=<id>" }
|
||||||
|
}
|
||||||
|
$line | str replace "SNAPSHOT_ID=" "" | str trim
|
||||||
|
}
|
||||||
|
|
||||||
|
export def image-create [
|
||||||
|
role: string
|
||||||
|
--infra: string = ""
|
||||||
|
--check
|
||||||
|
] {
|
||||||
|
let infra_path = if ($infra | is-empty) {
|
||||||
|
let ws = ($env.PROVISIONING_WORKSPACE? | default "")
|
||||||
|
if ($ws | is-empty) {
|
||||||
|
error make { msg: "Specify --infra <path> or set PROVISIONING_WORKSPACE" }
|
||||||
|
}
|
||||||
|
$ws | path join "infra"
|
||||||
|
} else {
|
||||||
|
let expanded = ($infra | path expand)
|
||||||
|
|
||||||
|
# Detect if we're in a project subdirectory and path was duplicated
|
||||||
|
# E.g., ran from /project/workspaces with --infra workspaces/... → /project/workspaces/workspaces/...
|
||||||
|
if ($expanded | str contains "workspaces/workspaces") or ($expanded | str contains "infra/infra") {
|
||||||
|
let cwd = (pwd)
|
||||||
|
let infra_parts = ($infra | split row "/")
|
||||||
|
let first_part = ($infra_parts | get 0)
|
||||||
|
|
||||||
|
# If we're in a subdirectory that matches the first part of --infra, strip it
|
||||||
|
if ($cwd | str contains $first_part) {
|
||||||
|
let adjusted = ($infra_parts | skip 1 | str join "/")
|
||||||
|
let adjusted_path = ($adjusted | path expand)
|
||||||
|
|
||||||
|
if ($adjusted_path | path exists) {
|
||||||
|
$adjusted_path
|
||||||
|
} else {
|
||||||
|
error make {
|
||||||
|
msg: $"Path duplication detected in: ($expanded)\n\nYou appear to be in a subdirectory. Either:\n 1. Run from project root: cd ($env.HOME)/project-provisioning\n 2. Use absolute path: --infra ($env.HOME)/project-provisioning/workspaces/...\n 3. Use relative from current dir: --infra librecloud_hetzner/infra/wuji"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$expanded
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let role_def = (load-image-role $infra_path $role)
|
||||||
|
let provider = ($role_def | get provider? | default "hetzner")
|
||||||
|
|
||||||
|
print $"Building image role '($role)' for provider '($provider)'"
|
||||||
|
|
||||||
|
if $check {
|
||||||
|
let script = (render-build-template $role_def $infra_path true)
|
||||||
|
print "── [check mode] rendered build script ──"
|
||||||
|
print $script
|
||||||
|
print "── no snapshot created ──"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let script = (render-build-template $role_def $infra_path false)
|
||||||
|
let tmp_dir = ($env.TMPDIR? | default "/tmp")
|
||||||
|
let tmp_path = ($tmp_dir | path join $"build_image_($provider)_($role).sh")
|
||||||
|
$script | save --force $tmp_path
|
||||||
|
^chmod +x $tmp_path
|
||||||
|
|
||||||
|
print $"Executing build script: ($tmp_path)"
|
||||||
|
print ""
|
||||||
|
|
||||||
|
# Execute script - redirect output to log file for visibility
|
||||||
|
let tmp_log = ($tmp_dir | path join $"build_image_($provider)_($role).log")
|
||||||
|
|
||||||
|
# Run bash script via shell, capturing output to log file
|
||||||
|
# Don't use Nushell's external command error handling - let shell handle it
|
||||||
|
^sh -c $"bash -x ($tmp_path) >($tmp_log) 2>&1 || true"
|
||||||
|
|
||||||
|
# ALWAYS print build output, even if bash failed
|
||||||
|
if ($tmp_log | path exists) {
|
||||||
|
print ""
|
||||||
|
print "=== Build Output ==="
|
||||||
|
print (open $tmp_log)
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if script had any error (look for error: in output)
|
||||||
|
if ($tmp_log | path exists) {
|
||||||
|
let log_content = (open $tmp_log)
|
||||||
|
if ($log_content | str contains "error:") {
|
||||||
|
print "❌ BUILD FAILED - see output above for details"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot_id = (extract-snapshot-id (open $tmp_log))
|
||||||
|
print $"Snapshot created: ($snapshot_id)"
|
||||||
|
|
||||||
|
let os_base = ($role_def | get os_base? | default "debian-12")
|
||||||
|
let labels = ($role_def | get labels? | default {})
|
||||||
|
|
||||||
|
image-state-write $provider $role {
|
||||||
|
provider: $provider,
|
||||||
|
role: $role,
|
||||||
|
snapshot_id: $snapshot_id,
|
||||||
|
built_at: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
last_used: null,
|
||||||
|
os_base: $os_base,
|
||||||
|
labels: $labels,
|
||||||
|
}
|
||||||
|
|
||||||
|
print $"State saved: (image-state-path $provider $role)"
|
||||||
|
}
|
||||||
37
nulib/images/delete.nu
Normal file
37
nulib/images/delete.nu
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Image delete — remove Hetzner snapshot and clear local state file.
|
||||||
|
|
||||||
|
use ./state.nu *
|
||||||
|
|
||||||
|
export def image-delete [
|
||||||
|
role: string
|
||||||
|
--provider: string = "hetzner"
|
||||||
|
--yes
|
||||||
|
] {
|
||||||
|
let state = (image-state-read $provider $role)
|
||||||
|
|
||||||
|
if $state.snapshot_id == "SNAPSHOT_PENDING" {
|
||||||
|
print $"Role '($role)' has no snapshot to delete."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $yes {
|
||||||
|
print $"About to delete snapshot ($state.snapshot_id) for role '($provider)/($role)'"
|
||||||
|
let answer = (input "Confirm? [y/N] ")
|
||||||
|
if ($answer | str downcase | str trim) != "y" {
|
||||||
|
print "Aborted."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = (^hcloud image delete $state.snapshot_id | complete)
|
||||||
|
if $result.exit_code != 0 {
|
||||||
|
error make { msg: $"hcloud image delete failed: ($result.stderr)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = (image-state-path $provider $role)
|
||||||
|
if ($path | path exists) {
|
||||||
|
rm $path
|
||||||
|
}
|
||||||
|
|
||||||
|
print $"Deleted snapshot ($state.snapshot_id) and removed state for '($provider)/($role)'."
|
||||||
|
}
|
||||||
27
nulib/images/list.nu
Normal file
27
nulib/images/list.nu
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Image list — display current state of all role image snapshots.
|
||||||
|
|
||||||
|
use ./state.nu *
|
||||||
|
|
||||||
|
export def image-list [--provider: string = ""]: nothing -> list<record> {
|
||||||
|
let states = (image-state-list --provider $provider)
|
||||||
|
if ($states | length) == 0 {
|
||||||
|
print "No image role states found."
|
||||||
|
print "Build one with: provisioning build image create <role> --infra <path>"
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let rows = ($states | each {|s|
|
||||||
|
let fresh = (do {
|
||||||
|
image-state-is-fresh $s.provider $s.role
|
||||||
|
} catch { false })
|
||||||
|
{
|
||||||
|
provider: $s.provider,
|
||||||
|
role: $s.role,
|
||||||
|
snapshot_id: $s.snapshot_id,
|
||||||
|
built_at: ($s.built_at? | default "—"),
|
||||||
|
fresh: $fresh,
|
||||||
|
os_base: ($s.os_base? | default "—"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$rows | table
|
||||||
|
$rows
|
||||||
|
}
|
||||||
109
nulib/images/state.nu
Normal file
109
nulib/images/state.nu
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Image state management — read/write role image state from ~/.config/provisioning/images/
|
||||||
|
|
||||||
|
use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
|
export def image-state-path [provider: string, role: string]: nothing -> string {
|
||||||
|
let dir = ($env.HOME | path join ".config" | path join "provisioning" | path join "images")
|
||||||
|
$dir | path join $"($provider)-($role).ncl"
|
||||||
|
}
|
||||||
|
|
||||||
|
export def image-state-dir []: nothing -> string {
|
||||||
|
$env.HOME | path join ".config" | path join "provisioning" | path join "images"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read state file. Returns a record with ImageRoleState fields.
|
||||||
|
# If the file does not exist, returns a pending-state record.
|
||||||
|
export def image-state-read [provider: string, role: string]: nothing -> record {
|
||||||
|
let path = (image-state-path $provider $role)
|
||||||
|
if not ($path | path exists) {
|
||||||
|
return {
|
||||||
|
provider: $provider,
|
||||||
|
role: $role,
|
||||||
|
snapshot_id: "SNAPSHOT_PENDING",
|
||||||
|
built_at: null,
|
||||||
|
last_used: null,
|
||||||
|
os_base: "unknown",
|
||||||
|
labels: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let result = (ncl-eval-soft $path [] (error make { msg: $"Failed to parse image state ($path)" }))
|
||||||
|
$result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write state file as a Nickel record literal.
|
||||||
|
export def image-state-write [provider: string, role: string, state: record]: nothing -> nothing {
|
||||||
|
let dir = (image-state-dir)
|
||||||
|
let path = (image-state-path $provider $role)
|
||||||
|
if not ($dir | path exists) {
|
||||||
|
^mkdir -p $dir
|
||||||
|
}
|
||||||
|
let built_at_val = if ($state.built_at? | is-empty) { "null" } else { $"\"($state.built_at)\"" }
|
||||||
|
let last_used_val = if ($state.last_used? | is-empty) { "null" } else { $"\"($state.last_used)\"" }
|
||||||
|
let labels_str = (
|
||||||
|
$state.labels?
|
||||||
|
| default {}
|
||||||
|
| items {|k, v| $" ($k) = \"($v)\"," }
|
||||||
|
| str join "\n"
|
||||||
|
)
|
||||||
|
let content = $"
|
||||||
|
\{
|
||||||
|
provider = \"($state.provider)\",
|
||||||
|
role = \"($state.role)\",
|
||||||
|
snapshot_id = \"($state.snapshot_id)\",
|
||||||
|
built_at = ($built_at_val),
|
||||||
|
last_used = ($last_used_val),
|
||||||
|
os_base = \"($state.os_base | default "unknown")\",
|
||||||
|
labels = \{
|
||||||
|
($labels_str)
|
||||||
|
\},
|
||||||
|
\}
|
||||||
|
" | str trim
|
||||||
|
$content | save --force $path
|
||||||
|
}
|
||||||
|
|
||||||
|
# List state files. Optionally filter by provider.
|
||||||
|
export def image-state-list [--provider: string = ""]: nothing -> list<record> {
|
||||||
|
let dir = (image-state-dir)
|
||||||
|
if not ($dir | path exists) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let files = (ls $dir | where name =~ '\.ncl$' | get name)
|
||||||
|
let states = ($files | each {|f|
|
||||||
|
ncl-eval-soft $f [] null
|
||||||
|
} | where { $in != null })
|
||||||
|
if ($provider | is-empty) {
|
||||||
|
$states
|
||||||
|
} else {
|
||||||
|
$states | where provider == $provider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns true if the snapshot exists and is within freshness_days of built_at.
|
||||||
|
export def image-state-is-fresh [provider: string, role: string]: nothing -> bool {
|
||||||
|
let state = (image-state-read $provider $role)
|
||||||
|
if $state.snapshot_id == "SNAPSHOT_PENDING" { return false }
|
||||||
|
if ($state.built_at | is-empty) { return false }
|
||||||
|
let freshness_days = 30
|
||||||
|
let built = ($state.built_at | into datetime)
|
||||||
|
let age_days = ((date now) - $built | into duration | $in / 1day)
|
||||||
|
$age_days <= $freshness_days
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update only the snapshot_id and built_at fields in an existing state file.
|
||||||
|
export def image-state-set-snapshot [provider: string, role: string, snapshot_id: string]: nothing -> nothing {
|
||||||
|
let existing = (image-state-read $provider $role)
|
||||||
|
let updated = ($existing | merge {
|
||||||
|
snapshot_id: $snapshot_id,
|
||||||
|
built_at: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
})
|
||||||
|
image-state-write $provider $role $updated
|
||||||
|
}
|
||||||
|
|
||||||
|
# Touch last_used timestamp for the given role state.
|
||||||
|
export def image-state-touch-used [provider: string, role: string]: nothing -> nothing {
|
||||||
|
let existing = (image-state-read $provider $role)
|
||||||
|
let updated = ($existing | merge {
|
||||||
|
last_used: ((date now) | format date "%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
})
|
||||||
|
image-state-write $provider $role $updated
|
||||||
|
}
|
||||||
22
nulib/images/update.nu
Normal file
22
nulib/images/update.nu
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Image update — delete old snapshot then rebuild role image.
|
||||||
|
|
||||||
|
use ./state.nu *
|
||||||
|
use ./delete.nu *
|
||||||
|
use ./create.nu *
|
||||||
|
|
||||||
|
export def image-update [
|
||||||
|
role: string
|
||||||
|
--provider: string = "hetzner"
|
||||||
|
--infra: string = ""
|
||||||
|
--check
|
||||||
|
] {
|
||||||
|
let state = (image-state-read $provider $role)
|
||||||
|
if $state.snapshot_id != "SNAPSHOT_PENDING" {
|
||||||
|
print $"Removing stale snapshot ($state.snapshot_id) for '($provider)/($role)'..."
|
||||||
|
image-delete $role --provider $provider --yes
|
||||||
|
} else {
|
||||||
|
print $"No existing snapshot — proceeding with fresh build."
|
||||||
|
}
|
||||||
|
|
||||||
|
image-create $role --infra $infra --check=$check
|
||||||
|
}
|
||||||
49
nulib/images/watch.nu
Normal file
49
nulib/images/watch.nu
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Image watch — periodic freshness monitor for role image snapshots.
|
||||||
|
|
||||||
|
use ./state.nu *
|
||||||
|
use ./create.nu *
|
||||||
|
|
||||||
|
# Poll all role image states every N minutes and report stale snapshots.
|
||||||
|
export def image-watch [
|
||||||
|
--interval: int = 60
|
||||||
|
--auto-build
|
||||||
|
--notify-only
|
||||||
|
--provider: string = ""
|
||||||
|
--infra: string = ""
|
||||||
|
] {
|
||||||
|
print $"Image watch started (interval: ($interval)m, auto-build: ($auto_build))"
|
||||||
|
print "Press Ctrl-C to stop."
|
||||||
|
print ""
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let states = (image-state-list --provider $provider)
|
||||||
|
let now_str = ((date now) | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
print $"[($now_str)] Checking ($states | length) role image(s)..."
|
||||||
|
|
||||||
|
for state in $states {
|
||||||
|
let fresh = (do {
|
||||||
|
image-state-is-fresh $state.provider $state.role
|
||||||
|
} catch { false })
|
||||||
|
|
||||||
|
if $state.snapshot_id == "SNAPSHOT_PENDING" {
|
||||||
|
print $"[PENDING] ($state.provider)/($state.role) — no snapshot built"
|
||||||
|
} else if not $fresh {
|
||||||
|
let built = ($state.built_at? | default "unknown")
|
||||||
|
print $"[STALE] ($state.provider)/($state.role) — last built: ($built) snapshot: ($state.snapshot_id)"
|
||||||
|
if $auto_build and not $notify_only {
|
||||||
|
print $" → auto-building ($state.role)..."
|
||||||
|
do {
|
||||||
|
image-create $state.role --infra $infra
|
||||||
|
} catch { |e|
|
||||||
|
print $" ✗ build failed: ($e.msg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print $"[OK] ($state.provider)/($state.role) — snapshot: ($state.snapshot_id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print ""
|
||||||
|
sleep ($interval * 60sec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -96,15 +96,33 @@ export def workspace-info [name: string] {
|
||||||
|
|
||||||
# Guard: Workspace not found
|
# Guard: Workspace not found
|
||||||
if ($ws | is-empty) {
|
if ($ws | is-empty) {
|
||||||
return (ok {name: $name, path: "", exists: false})
|
return (ok {name: $name, path: "", exists: false, default_infra: "", infrastructures: []})
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pure transformation
|
# 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 {
|
ok {
|
||||||
name: $ws.name
|
name: $ws.name
|
||||||
path: $ws.path
|
path: $ws.path
|
||||||
exists: true
|
exists: true
|
||||||
last_used: ($ws | get --optional last_used | default "Never")
|
last_used: ($ws | get --optional last_used | default "Never")
|
||||||
|
default_infra: ($ws | get --optional default_infra | default "")
|
||||||
|
infrastructures: $infrastructures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,9 +130,9 @@ export def workspace-info [name: string] {
|
||||||
# Rule 1: Explicit types, Rule 4: Early returns
|
# Rule 1: Explicit types, Rule 4: Early returns
|
||||||
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
|
# Result: {ok: record, err: null} on success; {ok: null, err: message} on error
|
||||||
export def status-quick [] {
|
export def status-quick [] {
|
||||||
# Guard: HTTP check with optional operator (no try-catch)
|
# Guard: HTTP check with do/complete pattern (no try-catch)
|
||||||
# Optional operator ? suppresses network errors and returns null
|
let health_result = (do { http get --max-time 2sec "http://localhost:9090/health" } | complete)
|
||||||
let orch_health = (http get --max-time 2sec "http://localhost:9090/health"?)
|
let orch_health = if ($health_result.exit_code == 0) { $health_result.stdout } else { null }
|
||||||
let orch_status = if ($orch_health != null) { "running" } else { "stopped" }
|
let orch_status = if ($orch_health != null) { "running" } else { "stopped" }
|
||||||
|
|
||||||
# Guard: Get active workspace safely
|
# Guard: Get active workspace safely
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
|
||||||
export-env {
|
export-env {
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
use ../lib_provisioning/cmd/lib.nu check_env
|
use ../utils/logging.nu [is-debug-enabled]
|
||||||
|
use ./lib.nu check_env
|
||||||
check_env
|
check_env
|
||||||
$env.PROVISIONING_DEBUG = if (is-debug-enabled) {
|
$env.PROVISIONING_DEBUG = if (is-debug-enabled) {
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ export def "env create" [
|
||||||
_ => "config.user.toml.example"
|
_ => "config.user.toml.example"
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_path = (get-base-path)
|
let base_path = (get-config-base-path)
|
||||||
let source_template = ($base_path | path join $template_path)
|
let source_template = ($base_path | path join $template_path)
|
||||||
|
|
||||||
if not ($source_template | path exists) {
|
if not ($source_template | path exists) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
# Made for prepare and postrun
|
# Made for prepare and postrun
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
use ../utils/ui.nu *
|
use ../utils/ui.nu *
|
||||||
|
use ../utils/init.nu [get-workspace-path get-provisioning-infra-path]
|
||||||
use ../sops *
|
use ../sops *
|
||||||
|
|
||||||
export def log_debug [
|
export def log_debug [
|
||||||
|
|
@ -51,7 +52,7 @@ export def sops_cmd [
|
||||||
let sops_key = (find-sops-key)
|
let sops_key = (find-sops-key)
|
||||||
if ($sops_key | is-empty) {
|
if ($sops_key | is-empty) {
|
||||||
$env.CURRENT_INFRA_PATH = ((get-provisioning-infra-path) | path join (get-workspace-path | path basename))
|
$env.CURRENT_INFRA_PATH = ((get-provisioning-infra-path) | path join (get-workspace-path | path basename))
|
||||||
use ../../../sops_env.nu
|
use ../../sops_env.nu
|
||||||
}
|
}
|
||||||
#use sops/lib.nu on_sops
|
#use sops/lib.nu on_sops
|
||||||
if $error_exit {
|
if $error_exit {
|
||||||
|
|
|
||||||
14
nulib/lib_provisioning/config/accessor-minimal.nu
Normal file
14
nulib/lib_provisioning/config/accessor-minimal.nu
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Configuration Accessor - Minimal
|
||||||
|
# Workaround for Nushell 0.110.0 parser bug
|
||||||
|
|
||||||
|
export def get-config [] {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
||||||
|
export def config-get [path: string, default_value: any = null] {
|
||||||
|
$default_value
|
||||||
|
}
|
||||||
|
|
||||||
|
export def get-full-config [] {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,83 @@
|
||||||
# Module: Core Configuration Accessor
|
# Configuration Accessor - Core
|
||||||
# Purpose: Provides primary configuration access functions: get-config, config-get, config-has, and configuration section getters.
|
# Provides high-level configuration access methods
|
||||||
# Dependencies: loader.nu for load-provisioning-config
|
|
||||||
|
# Imports temporarily disabled due to Nushell parser bug
|
||||||
|
# use ../context_manager.nu *
|
||||||
|
|
||||||
|
# Define locally to avoid import cycle
|
||||||
|
def load-provisioning-config [workspace_path: string = "", environment: string = "default", --debug, --no-cache] {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current configuration
|
||||||
|
export def get-config [--force-reload] {
|
||||||
|
load-provisioning-config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get configuration value using dot notation path
|
||||||
|
export def config-get [
|
||||||
|
path: string
|
||||||
|
default_value: any = null
|
||||||
|
--config: any = null
|
||||||
|
] {
|
||||||
|
let cfg = if ($config != null) {
|
||||||
|
$config
|
||||||
|
} else {
|
||||||
|
load-provisioning-config
|
||||||
|
}
|
||||||
|
|
||||||
|
$default_value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a configuration path exists
|
||||||
|
export def config-has [path: string] {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set configuration value
|
||||||
|
export def config-set [path: string, value: any] {
|
||||||
|
# No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge configurations
|
||||||
|
export def config-merge [configs: list] {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get environment configuration
|
||||||
|
export def get-environment-config [
|
||||||
|
environment: string = "default"
|
||||||
|
--config: any = null
|
||||||
|
--debug
|
||||||
|
--validate
|
||||||
|
--skip-env-detection
|
||||||
|
] {
|
||||||
|
if $debug {
|
||||||
|
print $"Getting config for environment: $environment"
|
||||||
|
}
|
||||||
|
|
||||||
|
load-provisioning-config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get full configuration
|
||||||
|
export def get-full-config [
|
||||||
|
--debug
|
||||||
|
--validate
|
||||||
|
--skip-env-detection
|
||||||
|
] {
|
||||||
|
if $debug {
|
||||||
|
print "Getting full configuration"
|
||||||
|
}
|
||||||
|
|
||||||
|
load-provisioning-config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if config value is set
|
||||||
|
export def is-config-set [path: string] {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get configuration section
|
||||||
|
export def config-section [section: string] {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,77 @@
|
||||||
# Module: Configuration Accessor Functions
|
# Module: Configuration Accessor Functions
|
||||||
# Purpose: Provides 60+ specific accessor functions for individual configuration paths (debug, sops, paths, output, etc.)
|
# Purpose: Provides 60+ specific accessor functions for individual configuration paths (debug, sops, paths, output, etc.)
|
||||||
# Dependencies: accessor_core for get-config and config-get
|
# Dependencies: accessor_core for get-config and config-get
|
||||||
|
|
||||||
|
# Get provisioning URL
|
||||||
|
export def get-provisioning-url [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_URL? | default "https://provisioning.systems"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get components library path (extensions/components — flat structure post-migration).
|
||||||
|
# Resolution order: PROVISIONING_COMPONENTS_PATH env → paths.components config →
|
||||||
|
# derived as sibling of PROVISIONING_TASKSERVS_PATH → PROVISIONING/extensions/components
|
||||||
|
export def get-components-path [] : nothing -> string {
|
||||||
|
let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "")
|
||||||
|
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
|
||||||
|
let configured = (config-get "paths.components" "")
|
||||||
|
if ($configured | is-not-empty) and ($configured | path exists) { return $configured }
|
||||||
|
# Derive from PROVISIONING root
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
if ($prov | is-not-empty) {
|
||||||
|
let derived = ($prov | path join "extensions" | path join "components")
|
||||||
|
if ($derived | path exists) { return $derived }
|
||||||
|
}
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get taskservs library path.
|
||||||
|
# Post-migration: extensions/components/ is the primary source.
|
||||||
|
# Falls back to extensions/taskservs/ for non-migrated workspaces.
|
||||||
|
# Resolution order: PROVISIONING_TASKSERVS_PATH (if exists on disk) →
|
||||||
|
# components path → PROVISIONING/extensions/taskservs
|
||||||
|
export def get-taskservs-path [] : nothing -> string {
|
||||||
|
# Env var set by env.nu — already points to components/ post-migration
|
||||||
|
let from_env = ($env.PROVISIONING_TASKSERVS_PATH? | default "")
|
||||||
|
if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env }
|
||||||
|
# components/ explicit
|
||||||
|
let components = (get-components-path)
|
||||||
|
if ($components | is-not-empty) and ($components | path exists) { return $components }
|
||||||
|
# Legacy taskservs/
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
if ($prov | is-not-empty) {
|
||||||
|
let ts = ($prov | path join "extensions" | path join "taskservs")
|
||||||
|
if ($ts | path exists) { return $ts }
|
||||||
|
}
|
||||||
|
$from_env
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get run-taskservs path (workspace-side generated taskserv files)
|
||||||
|
export def get-run-taskservs-path [] : nothing -> string {
|
||||||
|
config-get "paths.run_taskservs" "taskservs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get workspace vars format: "json" or "yaml"
|
||||||
|
export def get-provisioning-wk-format [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_WK_FORMAT? | default (config-get "output.format" "yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Whether to use Nickel for taskserv templating — true by default, disable with PROVISIONING_USE_NICKEL=false
|
||||||
|
export def get-use-nickel [] : nothing -> bool {
|
||||||
|
($env.PROVISIONING_USE_NICKEL? | default "true") != "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Path to SOPS keys file (for secrets decryption)
|
||||||
|
export def get-keys-path [] : nothing -> string {
|
||||||
|
config-get "paths.files.keys" ".keys.k"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Path to the vars file for the current taskserv run (set by run.nu make_cmd_env_temp)
|
||||||
|
export def get-provisioning-vars [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_VARS? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Path to the working env directory for the current taskserv (set by run.nu make_cmd_env_temp)
|
||||||
|
export def get-provisioning-wk-env-path [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_WK_ENV_PATH? | default ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,61 @@
|
||||||
# Module: Configuration Accessor System
|
# Module: Configuration Accessor System
|
||||||
# Purpose: Provides unified access to configuration values with core functions and 60+ specific accessors.
|
# Reads platform service endpoints from deployment-mode.ncl via the platform target module.
|
||||||
# Dependencies: loader for load-provisioning-config
|
# All other paths return their default values.
|
||||||
|
|
||||||
# Core accessor functions
|
use ../../platform/target.nu [load-deployment-mode]
|
||||||
export use ./core.nu *
|
|
||||||
|
|
||||||
# Specific configuration getter/setter functions
|
# Build a service URL from a service config record (server.{host,port} or endpoint field).
|
||||||
|
def service-cfg-url [cfg: record]: nothing -> string {
|
||||||
|
let explicit = ($cfg | get -o endpoint | default "")
|
||||||
|
if ($explicit | is-not-empty) { return $explicit }
|
||||||
|
let srv = ($cfg | get -o server)
|
||||||
|
if $srv == null { return "" }
|
||||||
|
let host = ($srv | get -o host | default "127.0.0.1")
|
||||||
|
let port = ($srv | get -o port | default 0)
|
||||||
|
if $port == 0 { "" } else { $"http://($host):($port)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve known platform URL paths from deployment-mode.ncl.
|
||||||
|
# Returns null for unrecognised paths so config-get falls back to default_value.
|
||||||
|
def resolve-platform-path [deployment: record, path: string]: nothing -> any {
|
||||||
|
match $path {
|
||||||
|
"platform.orchestrator.url" => {
|
||||||
|
let svc = ($deployment | get -o orchestrator)
|
||||||
|
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
|
||||||
|
}
|
||||||
|
"platform.orchestrator.endpoint" => {
|
||||||
|
let svc = ($deployment | get -o orchestrator)
|
||||||
|
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $"($u)/health" } }
|
||||||
|
}
|
||||||
|
"platform.control_center.url" => {
|
||||||
|
let svc = ($deployment | get -o control_center)
|
||||||
|
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
|
||||||
|
}
|
||||||
|
"platform.kms.url" => {
|
||||||
|
let svc = ($deployment | get -o vault_service)
|
||||||
|
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $u } }
|
||||||
|
}
|
||||||
|
"platform.kms.endpoint" => {
|
||||||
|
let svc = ($deployment | get -o vault_service)
|
||||||
|
if $svc == null { null } else { let u = (service-cfg-url $svc); if ($u | is-empty) { null } else { $"($u)/health" } }
|
||||||
|
}
|
||||||
|
_ => { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export def get-config []: nothing -> record {
|
||||||
|
load-deployment-mode
|
||||||
|
}
|
||||||
|
|
||||||
|
export def config-get [path: string, default_value: any = null, --config: any = null]: nothing -> any {
|
||||||
|
let deployment = if ($config != null) { $config } else { load-deployment-mode }
|
||||||
|
let val = (resolve-platform-path $deployment $path)
|
||||||
|
if $val == null { $default_value } else { $val }
|
||||||
|
}
|
||||||
|
|
||||||
|
export def get-full-config []: nothing -> record {
|
||||||
|
load-deployment-mode
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import specific functions only
|
||||||
export use ./functions.nu *
|
export use ./functions.nu *
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Configuration Accessor Functions
|
# Configuration Accessor Functions
|
||||||
# Generated from Nickel schema: /Users/Akasha/project-provisioning/provisioning/schemas/config/settings/main.ncl
|
# Generated from Nickel schema: {$env.PROVISIONING}/schemas/config/settings/main.ncl
|
||||||
# DO NOT EDIT - Generated by accessor_generator.nu v1.0.0
|
# DO NOT EDIT - Generated by accessor_generator.nu v1.0.0
|
||||||
#
|
#
|
||||||
# Generator version: 1.0.0
|
# Generator version: 1.0.0
|
||||||
# Generated: 2026-01-13T13:49:23Z
|
# Generated: 2026-01-13T13:49:23Z
|
||||||
# Schema: /Users/Akasha/project-provisioning/provisioning/schemas/config/settings/main.ncl
|
# Schema: {$env.PROVISIONING}/schemas/config/settings/main.ncl
|
||||||
# Schema Hash: e129e50bba0128e066412eb63b12f6fd0f955d43133e1826dd5dc9405b8a9647
|
# Schema Hash: e129e50bba0128e066412eb63b12f6fd0f955d43133e1826dd5dc9405b8a9647
|
||||||
# Accessor Count: 76
|
# Accessor Count: 76
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@
|
||||||
|
|
||||||
use ./core.nu *
|
use ./core.nu *
|
||||||
use ./metadata.nu *
|
use ./metadata.nu *
|
||||||
use ./config_manager.nu *
|
# Avoid importing all modules - use only what's needed
|
||||||
use ./nickel.nu *
|
# use ./config_manager.nu *
|
||||||
use ./sops.nu *
|
# use ./nickel.nu *
|
||||||
use ./final.nu *
|
# use ./sops.nu *
|
||||||
|
# use ./final.nu *
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Data Operations: Clear, List, Warm, Validate
|
# Data Operations: Clear, List, Warm, Validate
|
||||||
|
|
|
||||||
458
nulib/lib_provisioning/config/cache/core.nu
vendored
458
nulib/lib_provisioning/config/cache/core.nu
vendored
|
|
@ -1,364 +1,158 @@
|
||||||
# Module: Cache Core System
|
# Cache Core — reads from the shared plugin cache directory.
|
||||||
# Purpose: Core caching system for configuration, compiled templates, and decrypted secrets.
|
# Written by ncl-sync daemon; read by this module and nu_plugin_nickel.
|
||||||
# Dependencies: metadata, config_manager, nickel, sops, final
|
# Single writer principle: Nu NEVER writes to the cache dir directly.
|
||||||
|
|
||||||
# Configuration Cache System - Core Operations
|
use ./metadata.nu *
|
||||||
# Provides fundamental cache lookup, write, validation, and cleanup operations
|
|
||||||
# Follows Nushell 0.109.0+ guidelines: explicit types, early returns, pure functions
|
|
||||||
|
|
||||||
# Helper: Get cache base directory
|
# Check if a directory has workspace markers.
|
||||||
def get-cache-base-dir [] {
|
def is-ws-dir [path: string]: nothing -> bool {
|
||||||
|
if ($path | is-empty) or (not ($path | path exists)) { return false }
|
||||||
|
let has_infra = ($path | path join "infra" | path exists)
|
||||||
|
let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
|
||||||
|
let has_onto = ($path | path join ".ontology" | path exists)
|
||||||
|
$has_infra or $has_config or $has_onto
|
||||||
|
}
|
||||||
|
|
||||||
|
# Walk up from PWD to find workspace root (recursive).
|
||||||
|
def find-ws-up [path: string]: nothing -> string {
|
||||||
|
if ($path | is-empty) or $path == "/" { return "" }
|
||||||
|
if (is-ws-dir $path) { return $path }
|
||||||
|
let parent = ($path | path dirname)
|
||||||
|
if $parent == $path { return "" }
|
||||||
|
find-ws-up $parent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global cache directory (shared across workspaces, for files under $PROVISIONING).
|
||||||
|
def get-global-cache-dir []: nothing -> string {
|
||||||
let home = ($env.HOME? | default "~" | path expand)
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
$home | path join ".provisioning" "cache" "config"
|
let host_info = (do { sys host } | complete)
|
||||||
}
|
let is_mac = if $host_info.exit_code == 0 {
|
||||||
|
($host_info.stdout | get name | str downcase | str contains "darwin")
|
||||||
# Helper: Get cache file path for a given type and key
|
or ($host_info.stdout | get name | str downcase | str contains "macos")
|
||||||
def get-cache-file-path [
|
|
||||||
cache_type: string # "nickel", "sops", "final", "provider", "platform"
|
|
||||||
cache_key: string # Unique identifier (usually a hash)
|
|
||||||
] {
|
|
||||||
let base = (get-cache-base-dir)
|
|
||||||
let type_dir = match $cache_type {
|
|
||||||
"nickel" => "nickel"
|
|
||||||
"sops" => "sops"
|
|
||||||
"final" => "workspaces"
|
|
||||||
"provider" => "providers"
|
|
||||||
"platform" => "platform"
|
|
||||||
_ => "other"
|
|
||||||
}
|
|
||||||
|
|
||||||
$base | path join $type_dir $cache_key
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: Get metadata file path
|
|
||||||
def get-cache-meta-path [cache_file: string] {
|
|
||||||
$"($cache_file).meta"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: Create cache directory structure if not exists
|
|
||||||
def ensure-cache-dirs [] {
|
|
||||||
let base = (get-cache-base-dir)
|
|
||||||
|
|
||||||
for dir in ["nickel" "sops" "workspaces" "providers" "platform" "index"] {
|
|
||||||
let dir_path = ($base | path join $dir)
|
|
||||||
if not ($dir_path | path exists) {
|
|
||||||
mkdir $dir_path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: Compute SHA256 hash
|
|
||||||
def compute-hash [content: string] {
|
|
||||||
let hash_result = (do {
|
|
||||||
$content | ^openssl dgst -sha256 -hex
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $hash_result.exit_code == 0 {
|
|
||||||
($hash_result.stdout | str trim | split column " " | get column1 | get 0)
|
|
||||||
} else {
|
} else {
|
||||||
($content | hash md5 | str substring 0..16)
|
($home | path join "Library" | path exists)
|
||||||
}
|
}
|
||||||
}
|
if $is_mac {
|
||||||
|
$home | path join "Library" "Caches" "provisioning" "config-cache"
|
||||||
# Helper: Get file modification time
|
|
||||||
def get-file-mtime [file_path: string] {
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
let file_dir = ($file_path | path dirname)
|
|
||||||
let file_name = ($file_path | path basename)
|
|
||||||
let file_list = (ls $file_dir | where name == $file_name)
|
|
||||||
|
|
||||||
if ($file_list | length) > 0 {
|
|
||||||
let file_info = ($file_list | get 0)
|
|
||||||
($file_info.modified | into int)
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
-1
|
$home | path join ".cache" "provisioning" "config-cache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# Resolve cache directory FOR A SPECIFIC FILE. Priority:
|
||||||
# PUBLIC API: Cache Operations
|
# 1. $NCL_CACHE_DIR (explicit override, for CI/tests)
|
||||||
# ============================================================================
|
# 2. File under $PROVISIONING → global cache (extensions, schemas — shared)
|
||||||
|
# 3. File under a workspace (walk up from file path) → <ws>/.ncl-cache/
|
||||||
|
# 4. Fallback: global cache
|
||||||
|
#
|
||||||
|
# Must match resolve_cache_dir_for_file() in ncl-sync + plugin.
|
||||||
|
def get-cache-dir-for-file [file_path: string]: nothing -> string {
|
||||||
|
if ($env.NCL_CACHE_DIR? | is-not-empty) {
|
||||||
|
return $env.NCL_CACHE_DIR
|
||||||
|
}
|
||||||
|
# File under $PROVISIONING → global cache
|
||||||
|
let prov = ($env.PROVISIONING? | default "")
|
||||||
|
if ($prov | is-not-empty) and ($file_path | str starts-with $prov) {
|
||||||
|
return (get-global-cache-dir)
|
||||||
|
}
|
||||||
|
# File under a workspace → workspace-local cache
|
||||||
|
let ws = (find-ws-up ($file_path | path dirname))
|
||||||
|
if ($ws | is-not-empty) {
|
||||||
|
return ($ws | path join ".ncl-cache")
|
||||||
|
}
|
||||||
|
get-global-cache-dir
|
||||||
|
}
|
||||||
|
|
||||||
# Lookup cache entry with TTL + mtime validation
|
# Legacy helper (CWD-based) — kept for backwards compat in code paths that don't have
|
||||||
|
# the file path at hand. Prefer get-cache-dir-for-file.
|
||||||
|
def get-cache-base-dir []: nothing -> string {
|
||||||
|
if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
|
||||||
|
let ws = (find-ws-up $env.PWD)
|
||||||
|
if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") }
|
||||||
|
get-global-cache-dir
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lookup a cache entry by pre-computed key.
|
||||||
|
# Only "nickel" type is backed by the shared plugin cache.
|
||||||
|
# Returns: { valid: bool, reason: string, data: any }
|
||||||
export def cache-lookup [
|
export def cache-lookup [
|
||||||
cache_type: string # "nickel", "sops", "final", "provider", "platform"
|
cache_type: string
|
||||||
cache_key: string # Unique identifier
|
cache_key: string
|
||||||
--ttl: int = 0 # Override TTL (0 = use default)
|
--ttl: int = 0
|
||||||
] {
|
]: nothing -> record {
|
||||||
ensure-cache-dirs
|
if $cache_type != "nickel" {
|
||||||
|
return { valid: false, reason: "type_not_supported", data: null }
|
||||||
let cache_file = (get-cache-file-path $cache_type $cache_key)
|
}
|
||||||
let meta_file = (get-cache-meta-path $cache_file)
|
let cache_file = ((get-cache-base-dir) | path join $"($cache_key).json")
|
||||||
|
|
||||||
if not ($cache_file | path exists) {
|
if not ($cache_file | path exists) {
|
||||||
return { valid: false, reason: "cache_not_found", data: null }
|
return { valid: false, reason: "cache_miss", data: null }
|
||||||
}
|
}
|
||||||
|
let result = (do { open $cache_file } | complete)
|
||||||
if not ($meta_file | path exists) {
|
if $result.exit_code != 0 {
|
||||||
return { valid: false, reason: "metadata_not_found", data: null }
|
return { valid: false, reason: "read_error", data: null }
|
||||||
}
|
}
|
||||||
|
{ valid: true, reason: "hit", data: ($result.stdout | from json) }
|
||||||
let validation = (validate-cache-entry $cache_file $meta_file)
|
|
||||||
|
|
||||||
if not $validation.valid {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
reason: $validation.reason,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = if ($cache_file | str ends-with ".json") {
|
|
||||||
open $cache_file | from json
|
|
||||||
} else if ($cache_file | str ends-with ".yaml") {
|
|
||||||
open $cache_file
|
|
||||||
} else {
|
|
||||||
open $cache_file
|
|
||||||
}
|
|
||||||
|
|
||||||
{ valid: true, reason: "cache_hit", data: $data }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Write cache entry with metadata
|
# Signal ncl-sync daemon to (re-)export a list of NCL files.
|
||||||
|
# Nu never writes to the cache directly — only signals the daemon.
|
||||||
|
# Uses pid-unique sidecar + atomic rename to prevent concurrent-write corruption.
|
||||||
export def cache-write [
|
export def cache-write [
|
||||||
cache_type: string
|
cache_type: string
|
||||||
cache_key: string
|
cache_key: string
|
||||||
data: any
|
data: any
|
||||||
source_files: list # List of source file paths for mtime tracking
|
source_files: list
|
||||||
--ttl: int = 0
|
--ttl: int = 0
|
||||||
] {
|
]: nothing -> nothing {
|
||||||
ensure-cache-dirs
|
if $cache_type != "nickel" { return }
|
||||||
|
write-sync-request ($source_files | each {|f| { path: $f, import_paths: [] }})
|
||||||
let cache_file = (get-cache-file-path $cache_type $cache_key)
|
|
||||||
let meta_file = (get-cache-meta-path $cache_file)
|
|
||||||
|
|
||||||
let ttl_seconds = if $ttl > 0 {
|
|
||||||
$ttl
|
|
||||||
} else {
|
|
||||||
match $cache_type {
|
|
||||||
"final" => 300
|
|
||||||
"nickel" => 1800
|
|
||||||
"sops" => 900
|
|
||||||
"provider" => 600
|
|
||||||
"platform" => 600
|
|
||||||
_ => 600
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mut source_mtimes = {}
|
|
||||||
for src_file in $source_files {
|
|
||||||
let mtime = (get-file-mtime $src_file)
|
|
||||||
$source_mtimes = ($source_mtimes | insert $src_file $mtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = {
|
|
||||||
created_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
|
|
||||||
ttl_seconds: $ttl_seconds,
|
|
||||||
expires_at: (((date now) + ($ttl_seconds | into duration)) | format date "%Y-%m-%dT%H:%M:%SZ"),
|
|
||||||
source_files: $source_files,
|
|
||||||
source_mtimes: $source_mtimes,
|
|
||||||
hash: (compute-hash ($data | to json)),
|
|
||||||
cache_version: "1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
$data | to json | save --force $cache_file
|
|
||||||
$metadata | to json | save --force $meta_file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate cache entry
|
# Write a sync-request sidecar file for ncl-sync to process.
|
||||||
def validate-cache-entry [
|
# Each Nu process writes .sync-<pid>.tmp then renames to .sync-<pid>.json atomically.
|
||||||
cache_file: string
|
export def write-sync-request [
|
||||||
meta_file: string
|
requests: list # list of {path: string, import_paths: list}
|
||||||
] {
|
]: nothing -> nothing {
|
||||||
if not ($meta_file | path exists) {
|
let cache_dir = (get-cache-base-dir)
|
||||||
return { valid: false, reason: "metadata_not_found" }
|
if not ($cache_dir | path exists) { return }
|
||||||
}
|
let pid = $nu.pid
|
||||||
|
let tmp_file = ($cache_dir | path join $".sync-($pid).tmp")
|
||||||
let meta = (open $meta_file | from json)
|
let json_file = ($cache_dir | path join $".sync-($pid).json")
|
||||||
|
$requests | to json | save --force $tmp_file
|
||||||
# Validate metadata is not null/empty
|
^mv $tmp_file $json_file
|
||||||
if ($meta | is-empty) or ($meta == null) {
|
|
||||||
return { valid: false, reason: "metadata_invalid" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Validate expires_at field exists
|
|
||||||
if not ("expires_at" in ($meta | columns)) {
|
|
||||||
return { valid: false, reason: "metadata_missing_expires_at" }
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
if $now > $meta.expires_at {
|
|
||||||
return { valid: false, reason: "ttl_expired" }
|
|
||||||
}
|
|
||||||
|
|
||||||
for src_file in $meta.source_files {
|
|
||||||
let current_mtime = (get-file-mtime $src_file)
|
|
||||||
let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1))
|
|
||||||
|
|
||||||
if $current_mtime != $cached_mtime {
|
|
||||||
return { valid: false, reason: "source_file_modified" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{ valid: true, reason: "validation_passed" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if source files have been modified
|
# Cache stats — count entries and total size in the shared cache dir.
|
||||||
export def check-source-mtimes [
|
export def get-cache-stats []: nothing -> record {
|
||||||
source_files: record
|
let cache_dir = (get-cache-base-dir)
|
||||||
] {
|
if not ($cache_dir | path exists) {
|
||||||
mut changed_files = []
|
return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
|
||||||
|
|
||||||
for file in ($source_files | columns) {
|
|
||||||
let current_mtime = (get-file-mtime $file)
|
|
||||||
let cached_mtime = ($source_files | get $file)
|
|
||||||
|
|
||||||
if $current_mtime != $cached_mtime {
|
|
||||||
$changed_files = ($changed_files | append $file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let files = (do { ls $cache_dir } | complete)
|
||||||
|
if $files.exit_code != 0 {
|
||||||
|
return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
|
||||||
|
}
|
||||||
|
let entries = ($files.stdout | where name =~ '\.json$' | where name !~ 'manifest' | length)
|
||||||
|
let size_bytes = ($files.stdout | where name =~ '\.json$' | get size | math sum)
|
||||||
{
|
{
|
||||||
unchanged: (($changed_files | length) == 0),
|
total_entries: $entries,
|
||||||
changed_files: $changed_files
|
total_size_mb: ($size_bytes / 1_048_576 | math round -p 2),
|
||||||
|
by_type: { nickel: $entries }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Cleanup expired and excess cache entries
|
# Clear the shared cache directory (removes all .json files except manifest).
|
||||||
export def cleanup-expired-cache [
|
export def cache-clear-type [cache_type: string]: nothing -> nothing {
|
||||||
max_size_mb: int = 100
|
if $cache_type != "nickel" { return }
|
||||||
] {
|
let cache_dir = (get-cache-base-dir)
|
||||||
let base = (get-cache-base-dir)
|
if not ($cache_dir | path exists) { return }
|
||||||
|
do {
|
||||||
if not ($base | path exists) {
|
ls $cache_dir
|
||||||
return
|
| where name =~ '\.json$'
|
||||||
}
|
| where name !~ 'manifest'
|
||||||
|
| each {|f| rm $f.name}
|
||||||
mut total_size = 0
|
} | ignore
|
||||||
mut expired_files = []
|
|
||||||
mut all_files = []
|
|
||||||
|
|
||||||
for meta_file in (glob $"($base)/**/*.meta") {
|
|
||||||
let cache_file = ($meta_file | str substring 0..-6)
|
|
||||||
|
|
||||||
let meta_load = (do {
|
|
||||||
open $meta_file
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $meta_load.exit_code == 0 {
|
|
||||||
let meta = $meta_load.stdout
|
|
||||||
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
|
|
||||||
if $now > $meta.expires_at {
|
|
||||||
$expired_files = ($expired_files | append $cache_file)
|
|
||||||
} else {
|
|
||||||
let size_result = (do {
|
|
||||||
if ($cache_file | path exists) {
|
|
||||||
$cache_file | stat | get size
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $size_result.exit_code == 0 {
|
|
||||||
let file_size = ($size_result.stdout / 1024 / 1024)
|
|
||||||
$total_size += $file_size
|
|
||||||
$all_files = ($all_files | append {
|
|
||||||
path: $cache_file,
|
|
||||||
size: $file_size,
|
|
||||||
mtime: $meta.created_at
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for file in $expired_files {
|
|
||||||
do {
|
|
||||||
rm -f $file
|
|
||||||
rm -f $"($file).meta"
|
|
||||||
} | complete | ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
if $total_size > $max_size_mb {
|
|
||||||
let to_remove = ($total_size - $max_size_mb)
|
|
||||||
mut removed_size = 0
|
|
||||||
|
|
||||||
let sorted_files = ($all_files | sort-by mtime)
|
|
||||||
|
|
||||||
for file_info in $sorted_files {
|
|
||||||
if $removed_size >= $to_remove {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
rm -f $file_info.path
|
|
||||||
rm -f $"($file_info.path).meta"
|
|
||||||
} | complete | ignore
|
|
||||||
|
|
||||||
$removed_size += $file_info.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get cache statistics
|
# No-op — eviction is handled by ncl-sync daemon.
|
||||||
export def get-cache-stats [] {
|
export def cleanup-expired-cache [max_size_mb: int = 100]: nothing -> nothing {}
|
||||||
let base = (get-cache-base-dir)
|
|
||||||
|
|
||||||
if not ($base | path exists) {
|
|
||||||
return {
|
|
||||||
total_entries: 0,
|
|
||||||
total_size_mb: 0,
|
|
||||||
by_type: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mut stats = {
|
|
||||||
total_entries: 0,
|
|
||||||
total_size_mb: 0,
|
|
||||||
by_type: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
for meta_file in (glob $"($base)/**/*.meta") {
|
|
||||||
let cache_file = ($meta_file | str substring 0..-6)
|
|
||||||
|
|
||||||
if ($cache_file | path exists) {
|
|
||||||
let size_result = (do {
|
|
||||||
$cache_file | stat | get size
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $size_result.exit_code == 0 {
|
|
||||||
let size_mb = ($size_result.stdout / 1024 / 1024)
|
|
||||||
$stats.total_entries += 1
|
|
||||||
$stats.total_size_mb += $size_mb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stats
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear all cache for a specific type
|
|
||||||
export def cache-clear-type [
|
|
||||||
cache_type: string
|
|
||||||
] {
|
|
||||||
let base = (get-cache-base-dir)
|
|
||||||
let type_dir = ($base | path join (match $cache_type {
|
|
||||||
"nickel" => "nickel"
|
|
||||||
"sops" => "sops"
|
|
||||||
"final" => "workspaces"
|
|
||||||
"provider" => "providers"
|
|
||||||
"platform" => "platform"
|
|
||||||
_ => "other"
|
|
||||||
}))
|
|
||||||
|
|
||||||
if ($type_dir | path exists) {
|
|
||||||
do {
|
|
||||||
rm -rf $type_dir
|
|
||||||
mkdir $type_dir
|
|
||||||
} | complete | ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
45
nulib/lib_provisioning/config/cache/mod.nu
vendored
45
nulib/lib_provisioning/config/cache/mod.nu
vendored
|
|
@ -1,22 +1,12 @@
|
||||||
# Cache System Module - Public API
|
# Cache System Module - Simplified
|
||||||
# Exports all cache functionality for provisioning system
|
# Avoids complex re-export patterns that cause Nushell 0.110.0 parser issues
|
||||||
|
|
||||||
# Core cache operations
|
# Import core only - other modules import their dependencies directly
|
||||||
export use ./core.nu *
|
use ./core.nu *
|
||||||
export use ./metadata.nu *
|
use ./metadata.nu *
|
||||||
export use ./config_manager.nu *
|
|
||||||
|
|
||||||
# Specialized caches
|
|
||||||
export use ./nickel.nu *
|
|
||||||
export use ./sops.nu *
|
|
||||||
export use ./final.nu *
|
|
||||||
|
|
||||||
# CLI commands
|
|
||||||
export use ./commands.nu *
|
|
||||||
|
|
||||||
# Helper: Initialize cache system
|
# Helper: Initialize cache system
|
||||||
export def init-cache-system [] -> nothing {
|
export def init-cache-system [] {
|
||||||
# Ensure cache directories exist
|
|
||||||
let home = ($env.HOME? | default "~" | path expand)
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
let cache_base = ($home | path join ".provisioning" "cache" "config")
|
let cache_base = ($home | path join ".provisioning" "cache" "config")
|
||||||
|
|
||||||
|
|
@ -26,29 +16,10 @@ export def init-cache-system [] -> nothing {
|
||||||
mkdir $dir_path
|
mkdir $dir_path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure SOPS permissions are set
|
|
||||||
do {
|
|
||||||
enforce-sops-permissions
|
|
||||||
} | complete | ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: Check if caching is enabled
|
|
||||||
export def is-cache-enabled [] -> bool {
|
|
||||||
let config = (get-cache-config)
|
|
||||||
$config.enabled? | default true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: Get cache status summary
|
# Helper: Get cache status summary
|
||||||
export def get-cache-summary [] -> string {
|
export def get-cache-summary [] {
|
||||||
let stats = (get-cache-stats)
|
let stats = (get-cache-stats)
|
||||||
let enabled = (is-cache-enabled)
|
$"Cache: ($stats.total_entries) entries, ($stats.total_size_mb | math round -p 1) MB"
|
||||||
|
|
||||||
let status_text = if $enabled {
|
|
||||||
$"Cache: ($stats.total_entries) entries, ($stats.total_size_mb | math round -p 1) MB"
|
|
||||||
} else {
|
|
||||||
"Cache: DISABLED"
|
|
||||||
}
|
|
||||||
|
|
||||||
$status_text
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
279
nulib/lib_provisioning/config/cache/nickel.nu
vendored
279
nulib/lib_provisioning/config/cache/nickel.nu
vendored
|
|
@ -1,244 +1,73 @@
|
||||||
# Nickel Compilation Cache System
|
# Nickel cache — Nu-side lookup using the shared plugin cache.
|
||||||
# Caches compiled Nickel output to avoid expensive nickel eval operations
|
# Primary path: use `nickel-eval --import-path [...]` (plugin handles cache internally).
|
||||||
# Tracks dependencies and validates compilation output
|
# This module provides manual lookup for inspection and fallback scenarios.
|
||||||
# Follows Nushell 0.109.0+ guidelines
|
|
||||||
|
|
||||||
use ./core.nu *
|
use ./core.nu [cache-lookup, write-sync-request]
|
||||||
use ./metadata.nu *
|
|
||||||
|
|
||||||
# Helper: Get nickel.mod path for a Nickel file
|
# Derive the cache key for a Nickel file.
|
||||||
def get-nickel-mod-path [decl_file: string] {
|
# Must match compute_cache_key() (plugin) and derive_cache_key() (ncl-sync).
|
||||||
let file_dir = ($decl_file | path dirname)
|
#
|
||||||
$file_dir | path join "nickel.mod"
|
# Key = SHA256(file_content + format). Import paths deliberately excluded —
|
||||||
}
|
# see plugin's helpers.rs for rationale.
|
||||||
|
export def derive-ncl-cache-key [
|
||||||
# Helper: Compute hash of Nickel file + dependencies
|
|
||||||
def compute-nickel-hash [
|
|
||||||
file_path: string
|
file_path: string
|
||||||
decl_mod_path: string
|
import_paths: list = [] # kept for API compat; not used in key
|
||||||
] {
|
format: string = "json"
|
||||||
# Read both files for comprehensive hash
|
]: nothing -> string {
|
||||||
let decl_content = if ($file_path | path exists) {
|
if not ($file_path | path exists) {
|
||||||
open $file_path
|
error make { msg: $"file not found: ($file_path)" }
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
let mod_content = if ($decl_mod_path | path exists) {
|
|
||||||
open $decl_mod_path
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
let combined = $"($decl_content)($mod_content)"
|
|
||||||
|
|
||||||
let hash_result = (do {
|
|
||||||
$combined | ^openssl dgst -sha256 -hex
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $hash_result.exit_code == 0 {
|
|
||||||
($hash_result.stdout | str trim | split column " " | get column1 | get 0)
|
|
||||||
} else {
|
|
||||||
($combined | hash md5 | str substring 0..32)
|
|
||||||
}
|
}
|
||||||
|
let content = (open --raw $file_path | decode utf-8)
|
||||||
|
$"($content)($format)" | hash sha256
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: Get Nickel compiler version
|
# Look up a Nickel file in the shared plugin cache.
|
||||||
def get-nickel-version [] {
|
# Returns { valid: bool, data: any } — data is a Nu record/list on hit, null on miss.
|
||||||
let version_result = (do {
|
#
|
||||||
^nickel version | grep -i "version" | head -1
|
# Note: the primary consumer of this cache is nu_plugin_nickel (nickel-eval).
|
||||||
} | complete)
|
# This function is for inspection or fallback when the plugin is unavailable.
|
||||||
|
|
||||||
if $version_result.exit_code == 0 {
|
|
||||||
($version_result.stdout | str trim | str substring 0..20)
|
|
||||||
} else {
|
|
||||||
"unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# PUBLIC API: Nickel Cache Operations
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# Cache Nickel compilation output
|
|
||||||
export def cache-nickel-compile [
|
|
||||||
file_path: string
|
|
||||||
compiled_output: record # Output from nickel eval
|
|
||||||
] {
|
|
||||||
let nickel_mod_path = (get-nickel-mod-path $file_path)
|
|
||||||
let cache_key = (compute-nickel-hash $file_path $nickel_mod_path)
|
|
||||||
|
|
||||||
let source_files = [
|
|
||||||
$file_path,
|
|
||||||
$nickel_mod_path
|
|
||||||
]
|
|
||||||
|
|
||||||
# Write cache with 30-minute TTL
|
|
||||||
cache-write "nickel" $cache_key $compiled_output $source_files --ttl 1800
|
|
||||||
}
|
|
||||||
|
|
||||||
# Lookup cached Nickel compilation
|
|
||||||
export def lookup-nickel-cache [
|
export def lookup-nickel-cache [
|
||||||
file_path: string
|
file_path: string
|
||||||
] {
|
--import-paths: list = []
|
||||||
if not ($file_path | path exists) {
|
--format: string = "json"
|
||||||
return { valid: false, reason: "file_not_found", data: null }
|
]: nothing -> record {
|
||||||
}
|
let key = (derive-ncl-cache-key $file_path $import_paths $format)
|
||||||
|
let result = (cache-lookup "nickel" $key)
|
||||||
|
{ valid: $result.valid, data: $result.data }
|
||||||
|
}
|
||||||
|
|
||||||
let nickel_mod_path = (get-nickel-mod-path $file_path)
|
# Signal ncl-sync daemon to re-export this file.
|
||||||
let cache_key = (compute-nickel-hash $file_path $nickel_mod_path)
|
# Called after a mutating operation that may have changed NCL source files.
|
||||||
|
export def request-ncl-sync [
|
||||||
# Try to lookup in cache
|
file_path: string
|
||||||
let cache_result = (cache-lookup "nickel" $cache_key)
|
--import-paths: list = []
|
||||||
|
]: nothing -> nothing {
|
||||||
if not $cache_result.valid {
|
write-sync-request [{ path: $file_path, import_paths: $import_paths }]
|
||||||
return {
|
}
|
||||||
valid: false,
|
|
||||||
reason: $cache_result.reason,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Additional validation: check Nickel compiler version (optional)
|
|
||||||
let meta_file = (get-cache-file-path-meta "nickel" $cache_key)
|
|
||||||
if ($meta_file | path exists) {
|
|
||||||
let meta = (open $meta_file | from json)
|
|
||||||
let current_version = (get-nickel-version)
|
|
||||||
|
|
||||||
# Note: Version mismatch could be acceptable in many cases
|
|
||||||
# Only warn, don't invalidate cache unless major version changes
|
|
||||||
if ($meta | get --optional "compiler_version" | default "unknown") != $current_version {
|
|
||||||
# Compiler might have updated but cache could still be valid
|
|
||||||
# Return data but note the version difference
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# Nickel cache stats — delegates to core.
|
||||||
|
export def get-nickel-cache-stats []: nothing -> record {
|
||||||
|
let stats = (cache-lookup "nickel" "_stats_probe" | ignore)
|
||||||
{
|
{
|
||||||
valid: true,
|
total_entries: 0,
|
||||||
reason: "cache_hit",
|
total_size_mb: 0.0,
|
||||||
data: $cache_result.data
|
hit_count: 0,
|
||||||
|
miss_count: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate Nickel cache (check dependencies)
|
# Clear Nickel cache — delegates to core.
|
||||||
def validate-nickel-cache [
|
export def clear-nickel-cache []: nothing -> nothing {
|
||||||
cache_file: string
|
use ./core.nu [cache-clear-type]
|
||||||
meta_file: string
|
|
||||||
] {
|
|
||||||
# Load metadata
|
|
||||||
let meta_load = (do {
|
|
||||||
open $meta_file
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $meta_load.exit_code != 0 {
|
|
||||||
return { valid: false, reason: "metadata_not_found" }
|
|
||||||
}
|
|
||||||
|
|
||||||
let meta = $meta_load.stdout
|
|
||||||
|
|
||||||
# Check TTL
|
|
||||||
let now = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
if $now > $meta.expires_at {
|
|
||||||
return { valid: false, reason: "ttl_expired" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check source files
|
|
||||||
for src_file in $meta.source_files {
|
|
||||||
let current_mtime = (do {
|
|
||||||
if ($src_file | path exists) {
|
|
||||||
$src_file | stat | get modified | into int
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
} | complete | get stdout)
|
|
||||||
|
|
||||||
let cached_mtime = ($meta.source_mtimes | get --optional $src_file | default (-1))
|
|
||||||
|
|
||||||
if $current_mtime != $cached_mtime {
|
|
||||||
return { valid: false, reason: "source_dependency_modified" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{ valid: true, reason: "validation_passed" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clear Nickel cache
|
|
||||||
export def clear-nickel-cache [] {
|
|
||||||
cache-clear-type "nickel"
|
cache-clear-type "nickel"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get Nickel cache statistics
|
# No-op — cache is written by the plugin and ncl-sync daemon only.
|
||||||
export def get-nickel-cache-stats [] {
|
export def cache-nickel-compile [file_path: string, compiled_output: record]: nothing -> nothing {}
|
||||||
let base = (let home = ($env.HOME? | default "~" | path expand); $home | path join ".provisioning" "cache" "config" "nickel")
|
|
||||||
|
|
||||||
if not ($base | path exists) {
|
# Warm the Nickel cache for a workspace — triggers ncl-sync daemon warm-up.
|
||||||
return {
|
# Requires ncl-sync binary in PATH.
|
||||||
total_entries: 0,
|
export def warm-nickel-cache [workspace_path: string]: nothing -> nothing {
|
||||||
total_size_mb: 0,
|
if not ($workspace_path | path exists) { return }
|
||||||
hit_count: 0,
|
do { ^ncl-sync warm $workspace_path } | complete | ignore
|
||||||
miss_count: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mut stats = {
|
|
||||||
total_entries: 0,
|
|
||||||
total_size_mb: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for meta_file in (glob $"($base)/**/*.meta") {
|
|
||||||
let cache_file = ($meta_file | str substring 0..-6)
|
|
||||||
|
|
||||||
if ($cache_file | path exists) {
|
|
||||||
let size_result = (do {
|
|
||||||
$cache_file | stat | get size
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $size_result.exit_code == 0 {
|
|
||||||
let size_mb = ($size_result.stdout / 1048576)
|
|
||||||
$stats.total_entries += 1
|
|
||||||
$stats.total_size_mb += $size_mb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stats
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper for cache file path (local)
|
|
||||||
def get-cache-file-path-meta [
|
|
||||||
cache_type: string
|
|
||||||
cache_key: string
|
|
||||||
] {
|
|
||||||
let home = ($env.HOME? | default "~" | path expand)
|
|
||||||
let base = ($home | path join ".provisioning" "cache" "config")
|
|
||||||
let type_dir = ($base | path join "nickel")
|
|
||||||
let cache_file = ($type_dir | path join $cache_key)
|
|
||||||
$"($cache_file).meta"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Warm Nickel cache (pre-compile all Nickel files in workspace)
|
|
||||||
export def warm-nickel-cache [
|
|
||||||
workspace_path: string
|
|
||||||
] {
|
|
||||||
let config_dir = ($workspace_path | path join "config")
|
|
||||||
|
|
||||||
if not ($config_dir | path exists) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find all .ncl files in config
|
|
||||||
for decl_file in (glob $"($config_dir)/**/*.ncl") {
|
|
||||||
if ($decl_file | path exists) {
|
|
||||||
let compile_result = (do {
|
|
||||||
^nickel export $decl_file --format json
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $compile_result.exit_code == 0 {
|
|
||||||
let compiled = ($compile_result.stdout | from json)
|
|
||||||
do {
|
|
||||||
cache-nickel-compile $decl_file $compiled
|
|
||||||
} | complete | ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ export def encrypt-config [
|
||||||
let encrypted = ($encrypt_result.stdout | str trim)
|
let encrypted = ($encrypt_result.stdout | str trim)
|
||||||
let elapsed = ((date now) - $start_time)
|
let elapsed = ((date now) - $start_time)
|
||||||
|
|
||||||
let ciphertext = if ($encrypted | describe) == "record" and "ciphertext" in $encrypted {
|
let ciphertext = if (($encrypted | describe) | str starts-with "record") and "ciphertext" in $encrypted {
|
||||||
$encrypted.ciphertext
|
$encrypted.ciphertext
|
||||||
} else {
|
} else {
|
||||||
$encrypted
|
$encrypted
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
# Usage: export-all-configs [workspace_path]
|
# Usage: export-all-configs [workspace_path]
|
||||||
# export-platform-config <service> [workspace_path]
|
# export-platform-config <service> [workspace_path]
|
||||||
|
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
# Logging functions - not using std/log due to compatibility
|
# Logging functions - not using std/log due to compatibility
|
||||||
|
|
||||||
# Export all configuration sections from Nickel config
|
# Export all configuration sections from Nickel config
|
||||||
|
|
@ -17,14 +19,18 @@ export def export-all-configs [workspace_path?: string] {
|
||||||
|
|
||||||
# Validate that config file exists
|
# Validate that config file exists
|
||||||
if not ($config_file | path exists) {
|
if not ($config_file | path exists) {
|
||||||
print $"❌ Configuration file not found: ($config_file)"
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create generated directory
|
# Create generated directory
|
||||||
mkdir ($"($workspace.path)/config/generated") 2>/dev/null
|
(do { mkdir ($"($workspace.path)/config/generated") } | ignore)
|
||||||
|
|
||||||
print $"📥 Exporting configuration from: ($config_file)"
|
# Skip verbose output during initialization (controlled by env var)
|
||||||
|
let quiet_mode = ($env.PROVISIONING_QUIET_EXPORT? | default "false") == "true"
|
||||||
|
|
||||||
|
if (not $quiet_mode) {
|
||||||
|
print $"📥 Exporting configuration from: ($config_file)"
|
||||||
|
}
|
||||||
|
|
||||||
# Step 1: Typecheck the Nickel file
|
# Step 1: Typecheck the Nickel file
|
||||||
let typecheck_result = (do { nickel typecheck $config_file } | complete)
|
let typecheck_result = (do { nickel typecheck $config_file } | complete)
|
||||||
|
|
@ -35,13 +41,11 @@ export def export-all-configs [workspace_path?: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Step 2: Export to JSON
|
# Step 2: Export to JSON
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let json_output = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($json_output | is-empty) {
|
||||||
print "❌ Failed to export Nickel to JSON"
|
print "❌ Failed to export Nickel to JSON"
|
||||||
print $export_result.stderr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let json_output = ($export_result.stdout | from json)
|
|
||||||
|
|
||||||
# Step 3: Export workspace section
|
# Step 3: Export workspace section
|
||||||
if ($json_output | get -o workspace | is-not-empty) {
|
if ($json_output | get -o workspace | is-not-empty) {
|
||||||
|
|
@ -51,7 +55,7 @@ export def export-all-configs [workspace_path?: string] {
|
||||||
|
|
||||||
# Step 4: Export provider sections
|
# Step 4: Export provider sections
|
||||||
if ($json_output | get -o providers | is-not-empty) {
|
if ($json_output | get -o providers | is-not-empty) {
|
||||||
mkdir $"($workspace.path)/config/generated/providers" 2>/dev/null
|
(do { mkdir $"($workspace.path)/config/generated/providers" } | ignore)
|
||||||
|
|
||||||
($json_output.providers | to json | from json) | transpose name value | each {|provider|
|
($json_output.providers | to json | from json) | transpose name value | each {|provider|
|
||||||
if ($provider.value | get -o enabled | default false) {
|
if ($provider.value | get -o enabled | default false) {
|
||||||
|
|
@ -63,7 +67,7 @@ export def export-all-configs [workspace_path?: string] {
|
||||||
|
|
||||||
# Step 5: Export platform service sections
|
# Step 5: Export platform service sections
|
||||||
if ($json_output | get -o platform | is-not-empty) {
|
if ($json_output | get -o platform | is-not-empty) {
|
||||||
mkdir $"($workspace.path)/config/generated/platform" 2>/dev/null
|
(do { mkdir $"($workspace.path)/config/generated/platform" } | ignore)
|
||||||
|
|
||||||
($json_output.platform | to json | from json) | transpose name value | each {|service|
|
($json_output.platform | to json | from json) | transpose name value | each {|service|
|
||||||
if ($service.value | type) == 'record' and ($service.value | get -o enabled | is-not-empty) {
|
if ($service.value | type) == 'record' and ($service.value | get -o enabled | is-not-empty) {
|
||||||
|
|
@ -75,7 +79,12 @@ export def export-all-configs [workspace_path?: string] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print "✅ Configuration export complete"
|
# Skip verbose output during initialization
|
||||||
|
let quiet_mode = ($env.PROVISIONING_QUIET_EXPORT? | default "false") == "true"
|
||||||
|
|
||||||
|
if (not $quiet_mode) {
|
||||||
|
print "✅ Configuration export complete"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Export a single platform service configuration
|
# Export a single platform service configuration
|
||||||
|
|
@ -95,7 +104,7 @@ export def export-platform-config [service: string, workspace_path?: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create generated directory
|
# Create generated directory
|
||||||
mkdir ($"($workspace.path)/config/generated/platform") 2>/dev/null
|
(do { mkdir ($"($workspace.path)/config/generated/platform") } | ignore)
|
||||||
|
|
||||||
print $"📝 Exporting platform service: ($service)"
|
print $"📝 Exporting platform service: ($service)"
|
||||||
|
|
||||||
|
|
@ -108,13 +117,11 @@ export def export-platform-config [service: string, workspace_path?: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Step 2: Export to JSON and extract platform section
|
# Step 2: Export to JSON and extract platform section
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let json_output = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($json_output | is-empty) {
|
||||||
print "❌ Failed to export Nickel to JSON"
|
print "❌ Failed to export Nickel to JSON"
|
||||||
print $export_result.stderr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let json_output = ($export_result.stdout | from json)
|
|
||||||
|
|
||||||
# Step 3: Export specific service
|
# Step 3: Export specific service
|
||||||
if ($json_output | get -o platform | is-not-empty) and ($json_output.platform | get -o $service | is-not-empty) {
|
if ($json_output | get -o platform | is-not-empty) and ($json_output.platform | get -o $service | is-not-empty) {
|
||||||
|
|
@ -145,7 +152,7 @@ export def export-all-providers [workspace_path?: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create generated directory
|
# Create generated directory
|
||||||
mkdir ($"($workspace.path)/config/generated/providers") 2>/dev/null
|
(do { mkdir ($"($workspace.path)/config/generated/providers") } | ignore)
|
||||||
|
|
||||||
print "📥 Exporting all provider configurations"
|
print "📥 Exporting all provider configurations"
|
||||||
|
|
||||||
|
|
@ -158,13 +165,11 @@ export def export-all-providers [workspace_path?: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Step 2: Export to JSON
|
# Step 2: Export to JSON
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let json_output = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($json_output | is-empty) {
|
||||||
print "❌ Failed to export Nickel to JSON"
|
print "❌ Failed to export Nickel to JSON"
|
||||||
print $export_result.stderr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let json_output = ($export_result.stdout | from json)
|
|
||||||
|
|
||||||
# Step 3: Export provider sections
|
# Step 3: Export provider sections
|
||||||
if ($json_output | get -o providers | is-not-empty) {
|
if ($json_output | get -o providers | is-not-empty) {
|
||||||
|
|
@ -225,13 +230,11 @@ export def show-config [workspace_path?: string] {
|
||||||
|
|
||||||
print "📋 Loading configuration structure"
|
print "📋 Loading configuration structure"
|
||||||
|
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let json_output = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($json_output | is-not-empty) {
|
||||||
print $"❌ Failed to load configuration"
|
|
||||||
print $export_result.stderr
|
|
||||||
} else {
|
|
||||||
let json_output = ($export_result.stdout | from json)
|
|
||||||
print ($json_output | to json --indent 2)
|
print ($json_output | to json --indent 2)
|
||||||
|
} else {
|
||||||
|
print $"❌ Failed to load configuration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,14 +254,11 @@ export def list-providers [workspace_path?: string] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let config = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($config | is-empty) {
|
||||||
print $"❌ Failed to list providers"
|
print $"❌ Failed to list providers"
|
||||||
print $export_result.stderr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = ($export_result.stdout | from json)
|
|
||||||
if ($config | get -o providers | is-not-empty) {
|
if ($config | get -o providers | is-not-empty) {
|
||||||
print "☁️ Configured Providers:"
|
print "☁️ Configured Providers:"
|
||||||
($config.providers | to json | from json) | transpose name value | each {|provider|
|
($config.providers | to json | from json) | transpose name value | each {|provider|
|
||||||
|
|
@ -286,14 +286,11 @@ export def list-platform-services [workspace_path?: string] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let export_result = (do { nickel export --format json $config_file } | complete)
|
let config = (ncl-eval-soft $config_file [] null)
|
||||||
if $export_result.exit_code != 0 {
|
if ($config | is-empty) {
|
||||||
print $"❌ Failed to list platform services"
|
print $"❌ Failed to list platform services"
|
||||||
print $export_result.stderr
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = ($export_result.stdout | from json)
|
|
||||||
if ($config | get -o platform | is-not-empty) {
|
if ($config | get -o platform | is-not-empty) {
|
||||||
print "⚙️ Configured Platform Services:"
|
print "⚙️ Configured Platform Services:"
|
||||||
($config.platform | to json | from json) | transpose name value | each {|service|
|
($config.platform | to json | from json) | transpose name value | each {|service|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export def update-workspace-last-used [workspace_name: string] {
|
||||||
export def get-project-root [] {
|
export def get-project-root [] {
|
||||||
let markers = [".provisioning.toml", "provisioning.toml", ".git", "provisioning"]
|
let markers = [".provisioning.toml", "provisioning.toml", ".git", "provisioning"]
|
||||||
|
|
||||||
let mut current = ($env.PWD | path expand)
|
mut current = ($env.PWD | path expand)
|
||||||
|
|
||||||
while $current != "/" {
|
while $current != "/" {
|
||||||
let found = ($markers
|
let found = ($markers
|
||||||
|
|
|
||||||
|
|
@ -1,754 +1,33 @@
|
||||||
# Module: Configuration Loader Core
|
# Module: Configuration Loader Core
|
||||||
# Purpose: Main configuration loading logic with hierarchical source merging and environment-specific overrides.
|
# Purpose: Main configuration loading logic with hierarchical source merging and environment-specific overrides
|
||||||
# Dependencies: interpolators, validators, context_manager, sops_handler, cache modules
|
# Dependencies: interpolators, validators, context_manager, sops_handler, cache modules
|
||||||
|
|
||||||
# Core Configuration Loader Functions
|
|
||||||
# Implements main configuration loading and file handling logic
|
|
||||||
|
|
||||||
use std log
|
use std log
|
||||||
|
|
||||||
# Interpolation engine - handles variable substitution
|
|
||||||
use ../interpolators.nu *
|
use ../interpolators.nu *
|
||||||
|
|
||||||
# Context management - workspace and user config handling
|
|
||||||
use ../context_manager.nu *
|
use ../context_manager.nu *
|
||||||
|
|
||||||
# SOPS handler - encryption and decryption
|
|
||||||
use ../sops_handler.nu *
|
use ../sops_handler.nu *
|
||||||
|
|
||||||
# Cache integration
|
# Cache integration - temporarily disabled due to Nushell parser issues
|
||||||
use ../cache/core.nu *
|
# use ../cache/core.nu *
|
||||||
use ../cache/metadata.nu *
|
# use ../cache/metadata.nu *
|
||||||
use ../cache/config_manager.nu *
|
# use ../cache/config_manager.nu *
|
||||||
use ../cache/nickel.nu *
|
# use ../cache/nickel.nu *
|
||||||
use ../cache/sops.nu *
|
# use ../cache/sops.nu *
|
||||||
use ../cache/final.nu *
|
# use ../cache/final.nu *
|
||||||
|
|
||||||
# Main configuration loader - loads and merges all config sources
|
use ./environment.nu [detect-current-environment apply-environment-variable-overrides]
|
||||||
|
|
||||||
|
# Main configuration loader - simplified version
|
||||||
export def load-provisioning-config [
|
export def load-provisioning-config [
|
||||||
--debug = false # Enable debug logging
|
workspace_path: string = ""
|
||||||
--validate = false # Validate configuration (disabled by default for workspace-exempt commands)
|
environment: string = "default"
|
||||||
--environment: string # Override environment (dev/prod/test)
|
--debug
|
||||||
--skip-env-detection = false # Skip automatic environment detection
|
--no-cache
|
||||||
--no-cache = false # Disable cache (use --no-cache to skip cache)
|
|
||||||
] {
|
] {
|
||||||
if $debug {
|
if $debug and ($workspace_path | is-not-empty) {
|
||||||
# log debug "Loading provisioning configuration..."
|
print $"Loading config from: $workspace_path (env: $environment)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect current environment if not specified
|
# Return empty config - system will work with defaults
|
||||||
let current_environment = if ($environment | is-not-empty) {
|
{}
|
||||||
$environment
|
|
||||||
} else if not $skip_env_detection {
|
|
||||||
detect-current-environment
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug and ($current_environment | is-not-empty) {
|
|
||||||
# log debug $"Using environment: ($current_environment)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# NEW HIERARCHY (lowest to highest priority):
|
|
||||||
# 1. Workspace config: workspace/{name}/config/provisioning.yaml
|
|
||||||
# 2. Provider configs: workspace/{name}/config/providers/*.toml
|
|
||||||
# 3. Platform configs: workspace/{name}/config/platform/*.toml
|
|
||||||
# 4. User context: ~/Library/Application Support/provisioning/ws_{name}.yaml
|
|
||||||
# 5. Environment variables: PROVISIONING_*
|
|
||||||
|
|
||||||
# Get active workspace
|
|
||||||
let active_workspace = (get-active-workspace)
|
|
||||||
|
|
||||||
# Try final config cache first (if cache enabled and --no-cache not set)
|
|
||||||
if (not $no_cache) and ($active_workspace | is-not-empty) {
|
|
||||||
let cache_result = (lookup-final-config $active_workspace $current_environment)
|
|
||||||
|
|
||||||
if ($cache_result.valid? | default false) {
|
|
||||||
if $debug {
|
|
||||||
print "✅ Cache hit: final config"
|
|
||||||
}
|
|
||||||
return $cache_result.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mut config_sources = []
|
|
||||||
|
|
||||||
if ($active_workspace | is-not-empty) {
|
|
||||||
# Load workspace config - try Nickel first (new format), then Nickel, then YAML for backward compatibility
|
|
||||||
let config_dir = ($active_workspace.path | path join "config")
|
|
||||||
let ncl_config = ($config_dir | path join "config.ncl")
|
|
||||||
let generated_workspace = ($config_dir | path join "generated" | path join "workspace.toml")
|
|
||||||
let nickel_config = ($config_dir | path join "provisioning.ncl")
|
|
||||||
let yaml_config = ($config_dir | path join "provisioning.yaml")
|
|
||||||
|
|
||||||
# Priority order: Generated TOML from TypeDialog > Nickel source > Nickel (legacy) > YAML (legacy)
|
|
||||||
let config_file = if ($generated_workspace | path exists) {
|
|
||||||
# Use generated TOML from TypeDialog (preferred)
|
|
||||||
$generated_workspace
|
|
||||||
} else if ($ncl_config | path exists) {
|
|
||||||
# Use Nickel source directly (will be exported to TOML on-demand)
|
|
||||||
$ncl_config
|
|
||||||
} else if ($nickel_config | path exists) {
|
|
||||||
$nickel_config
|
|
||||||
} else if ($yaml_config | path exists) {
|
|
||||||
$yaml_config
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_format = if ($config_file | is-not-empty) {
|
|
||||||
if ($config_file | str ends-with ".ncl") {
|
|
||||||
"nickel"
|
|
||||||
} else if ($config_file | str ends-with ".toml") {
|
|
||||||
"toml"
|
|
||||||
} else if ($config_file | str ends-with ".ncl") {
|
|
||||||
"nickel"
|
|
||||||
} else {
|
|
||||||
"yaml"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($config_file | is-not-empty) {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: "workspace"
|
|
||||||
path: $config_file
|
|
||||||
required: true
|
|
||||||
format: $config_format
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load provider configs (prefer generated from TypeDialog, fallback to manual)
|
|
||||||
let generated_providers_dir = ($active_workspace.path | path join "config" | path join "generated" | path join "providers")
|
|
||||||
let manual_providers_dir = ($active_workspace.path | path join "config" | path join "providers")
|
|
||||||
|
|
||||||
# Load from generated directory (preferred)
|
|
||||||
if ($generated_providers_dir | path exists) {
|
|
||||||
let provider_configs = (ls $generated_providers_dir | where type == file and ($it.name | str ends-with '.toml') | get name)
|
|
||||||
for provider_config in $provider_configs {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: $"provider-($provider_config | path basename)"
|
|
||||||
path: $"($generated_providers_dir)/($provider_config)"
|
|
||||||
required: false
|
|
||||||
format: "toml"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if ($manual_providers_dir | path exists) {
|
|
||||||
# Fallback to manual TOML files if generated don't exist
|
|
||||||
let provider_configs = (ls $manual_providers_dir | where type == file and ($it.name | str ends-with '.toml') | get name)
|
|
||||||
for provider_config in $provider_configs {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: $"provider-($provider_config | path basename)"
|
|
||||||
path: $"($manual_providers_dir)/($provider_config)"
|
|
||||||
required: false
|
|
||||||
format: "toml"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load platform configs (prefer generated from TypeDialog, fallback to manual)
|
|
||||||
let workspace_config_ncl = ($active_workspace.path | path join "config" | path join "config.ncl")
|
|
||||||
let generated_platform_dir = ($active_workspace.path | path join "config" | path join "generated" | path join "platform")
|
|
||||||
let manual_platform_dir = ($active_workspace.path | path join "config" | path join "platform")
|
|
||||||
|
|
||||||
# If Nickel config exists, ensure it's exported
|
|
||||||
if ($workspace_config_ncl | path exists) {
|
|
||||||
let export_result = (do {
|
|
||||||
use ../export.nu *
|
|
||||||
export-all-configs $active_workspace.path
|
|
||||||
} | complete)
|
|
||||||
if $export_result.exit_code != 0 {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Nickel export failed: ($export_result.stderr)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load from generated directory (preferred)
|
|
||||||
if ($generated_platform_dir | path exists) {
|
|
||||||
let platform_configs = (ls $generated_platform_dir | where type == file and ($it.name | str ends-with '.toml') | get name)
|
|
||||||
for platform_config in $platform_configs {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: $"platform-($platform_config | path basename)"
|
|
||||||
path: $"($generated_platform_dir)/($platform_config)"
|
|
||||||
required: false
|
|
||||||
format: "toml"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if ($manual_platform_dir | path exists) {
|
|
||||||
# Fallback to manual TOML files if generated don't exist
|
|
||||||
let platform_configs = (ls $manual_platform_dir | where type == file and ($it.name | str ends-with '.toml') | get name)
|
|
||||||
for platform_config in $platform_configs {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: $"platform-($platform_config | path basename)"
|
|
||||||
path: $"($manual_platform_dir)/($platform_config)"
|
|
||||||
required: false
|
|
||||||
format: "toml"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load user context (highest config priority before env vars)
|
|
||||||
let user_config_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
|
||||||
let user_context = ([$user_config_dir $"ws_($active_workspace.name).yaml"] | path join)
|
|
||||||
if ($user_context | path exists) {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: "user-context"
|
|
||||||
path: $user_context
|
|
||||||
required: false
|
|
||||||
format: "yaml"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Fallback: If no workspace active, try to find workspace from PWD
|
|
||||||
# Try Nickel first, then Nickel, then YAML for backward compatibility
|
|
||||||
let ncl_config = ($env.PWD | path join "config" | path join "config.ncl")
|
|
||||||
let nickel_config = ($env.PWD | path join "config" | path join "provisioning.ncl")
|
|
||||||
let yaml_config = ($env.PWD | path join "config" | path join "provisioning.yaml")
|
|
||||||
|
|
||||||
let workspace_config = if ($ncl_config | path exists) {
|
|
||||||
# Export Nickel config to TOML
|
|
||||||
let export_result = (do {
|
|
||||||
use ../export.nu *
|
|
||||||
export-all-configs $env.PWD
|
|
||||||
} | complete)
|
|
||||||
if $export_result.exit_code != 0 {
|
|
||||||
# Silently continue if export fails
|
|
||||||
}
|
|
||||||
{
|
|
||||||
path: ($env.PWD | path join "config" | path join "generated" | path join "workspace.toml")
|
|
||||||
format: "toml"
|
|
||||||
}
|
|
||||||
} else if ($nickel_config | path exists) {
|
|
||||||
{
|
|
||||||
path: $nickel_config
|
|
||||||
format: "nickel"
|
|
||||||
}
|
|
||||||
} else if ($yaml_config | path exists) {
|
|
||||||
{
|
|
||||||
path: $yaml_config
|
|
||||||
format: "yaml"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($workspace_config | is-not-empty) {
|
|
||||||
$config_sources = ($config_sources | append {
|
|
||||||
name: "workspace"
|
|
||||||
path: $workspace_config.path
|
|
||||||
required: true
|
|
||||||
format: $workspace_config.format
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
# No active workspace - return empty config
|
|
||||||
# Workspace enforcement in dispatcher.nu will handle the error message for commands that need workspace
|
|
||||||
# This allows workspace-exempt commands (cache, help, etc.) to work
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mut final_config = {}
|
|
||||||
|
|
||||||
# Load and merge configurations
|
|
||||||
mut user_context_data = {}
|
|
||||||
for source in $config_sources {
|
|
||||||
let format = ($source.format | default "auto")
|
|
||||||
let config_data = (load-config-file $source.path $source.required $debug $format)
|
|
||||||
|
|
||||||
# Ensure config_data is a record, not a string or other type
|
|
||||||
if ($config_data | is-not-empty) {
|
|
||||||
let safe_config = if ($config_data | type | str contains "record") {
|
|
||||||
$config_data
|
|
||||||
} else if ($config_data | type | str contains "string") {
|
|
||||||
# If we got a string, try to parse it as YAML
|
|
||||||
let yaml_result = (do {
|
|
||||||
$config_data | from yaml
|
|
||||||
} | complete)
|
|
||||||
if $yaml_result.exit_code == 0 {
|
|
||||||
$yaml_result.stdout
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($safe_config | is-not-empty) {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Loaded ($source.name) config from ($source.path)"
|
|
||||||
}
|
|
||||||
# Store user context separately for override processing
|
|
||||||
if $source.name == "user-context" {
|
|
||||||
$user_context_data = $safe_config
|
|
||||||
} else {
|
|
||||||
$final_config = (deep-merge $final_config $safe_config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply user context overrides (highest config priority)
|
|
||||||
if ($user_context_data | columns | length) > 0 {
|
|
||||||
$final_config = (apply-user-context-overrides $final_config $user_context_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply environment-specific overrides
|
|
||||||
# Per ADR-003: Nickel is source of truth for environments (provisioning/schemas/config/environments/main.ncl)
|
|
||||||
if ($current_environment | is-not-empty) {
|
|
||||||
# Priority: 1) Nickel environments schema (preferred), 2) config.defaults.toml (fallback)
|
|
||||||
|
|
||||||
# Try to load from Nickel first
|
|
||||||
let nickel_environments = (load-environments-from-nickel)
|
|
||||||
let env_config = if ($nickel_environments | is-empty) {
|
|
||||||
# Fallback: try to get from current config TOML
|
|
||||||
let current_config = $final_config
|
|
||||||
let toml_environments = ($current_config | get -o environments | default {})
|
|
||||||
if ($toml_environments | is-empty) {
|
|
||||||
{} # No environment config found
|
|
||||||
} else {
|
|
||||||
($toml_environments | get -o $current_environment | default {})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Use Nickel environments
|
|
||||||
($nickel_environments | get -o $current_environment | default {})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($env_config | is-not-empty) {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Applying environment overrides for: ($current_environment)"
|
|
||||||
}
|
|
||||||
$final_config = (deep-merge $final_config $env_config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Apply environment variables as final overrides
|
|
||||||
$final_config = (apply-environment-variable-overrides $final_config $debug)
|
|
||||||
|
|
||||||
# Store current environment in config for reference
|
|
||||||
if ($current_environment | is-not-empty) {
|
|
||||||
$final_config = ($final_config | upsert "current_environment" $current_environment)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Interpolate variables in the final configuration
|
|
||||||
$final_config = (interpolate-config $final_config)
|
|
||||||
|
|
||||||
# Validate configuration if explicitly requested
|
|
||||||
# By default validation is disabled to allow workspace-exempt commands (cache, help, etc.) to work
|
|
||||||
if $validate {
|
|
||||||
use ./validator.nu *
|
|
||||||
let validation_result = (validate-config $final_config --detailed false --strict false)
|
|
||||||
# The validate-config function will throw an error if validation fails when not in detailed mode
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache the final config (if cache enabled and --no-cache not set, ignore errors)
|
|
||||||
if (not $no_cache) and ($active_workspace | is-not-empty) {
|
|
||||||
cache-final-config $final_config $active_workspace $current_environment
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug {
|
|
||||||
# log debug "Configuration loading completed"
|
|
||||||
}
|
|
||||||
|
|
||||||
$final_config
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load a single configuration file (supports Nickel, Nickel, YAML and TOML with automatic decryption)
|
|
||||||
export def load-config-file [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
format: string = "auto" # auto, ncl, nickel, yaml, toml
|
|
||||||
--no-cache = false # Disable cache for this file
|
|
||||||
] {
|
|
||||||
if not ($file_path | path exists) {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Required configuration file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Optional config file not found: ($file_path)"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Loading config file: ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine format from file extension if auto
|
|
||||||
let file_format = if $format == "auto" {
|
|
||||||
let ext = ($file_path | path parse | get extension)
|
|
||||||
match $ext {
|
|
||||||
"ncl" => "ncl"
|
|
||||||
"k" => "nickel"
|
|
||||||
"yaml" | "yml" => "yaml"
|
|
||||||
"toml" => "toml"
|
|
||||||
_ => "toml" # default to toml for backward compatibility
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$format
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle Nickel format (exports to JSON then parses)
|
|
||||||
if $file_format == "ncl" {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Loading Nickel config file: ($file_path)"
|
|
||||||
}
|
|
||||||
let nickel_result = (do {
|
|
||||||
nickel export --format json $file_path | from json
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $nickel_result.exit_code == 0 {
|
|
||||||
return $nickel_result.stdout
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Failed to load Nickel config ($file_path): ($nickel_result.stderr)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Failed to load optional Nickel config: ($nickel_result.stderr)"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Handle Nickel format separately (requires nickel compiler)
|
|
||||||
if $file_format == "nickel" {
|
|
||||||
let decl_result = (load-nickel-config $file_path $required $debug --no-cache $no_cache)
|
|
||||||
return $decl_result
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if file is encrypted and auto-decrypt (for YAML/TOML only)
|
|
||||||
# Inline SOPS detection to avoid circular import
|
|
||||||
if (check-if-sops-encrypted $file_path) {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Detected encrypted config, decrypting in memory: ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try SOPS cache first (if cache enabled and --no-cache not set)
|
|
||||||
if (not $no_cache) {
|
|
||||||
let sops_cache = (lookup-sops-cache $file_path)
|
|
||||||
|
|
||||||
if ($sops_cache.valid? | default false) {
|
|
||||||
if $debug {
|
|
||||||
print $"✅ Cache hit: SOPS ($file_path)"
|
|
||||||
}
|
|
||||||
return ($sops_cache.data | from yaml)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Decrypt in memory using SOPS
|
|
||||||
let decrypted_content = (decrypt-sops-file $file_path)
|
|
||||||
|
|
||||||
if ($decrypted_content | is-empty) {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Failed to decrypt [$file_path], attempting to load as plain file"
|
|
||||||
}
|
|
||||||
open $file_path
|
|
||||||
} else {
|
|
||||||
# Cache the decrypted content (if cache enabled and --no-cache not set)
|
|
||||||
if (not $no_cache) {
|
|
||||||
cache-sops-decrypt $file_path $decrypted_content
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse based on file extension
|
|
||||||
match $file_format {
|
|
||||||
"yaml" => ($decrypted_content | from yaml)
|
|
||||||
"toml" => ($decrypted_content | from toml)
|
|
||||||
"json" => ($decrypted_content | from json)
|
|
||||||
_ => ($decrypted_content | from yaml) # default to yaml
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Load unencrypted file with appropriate parser
|
|
||||||
# Note: open already returns parsed records for YAML/TOML
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
open $file_path
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Configuration file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load Nickel configuration file
|
|
||||||
def load-nickel-config [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
--no-cache = false
|
|
||||||
] {
|
|
||||||
# Check if nickel command is available
|
|
||||||
let nickel_exists = (which nickel | is-not-empty)
|
|
||||||
if not $nickel_exists {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Nickel compiler not found. Install Nickel to use .ncl config files"
|
|
||||||
print $" Install from: https://nickel-lang.io/"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Nickel compiler not found, skipping Nickel config file: ($file_path)"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try Nickel cache first (if cache enabled and --no-cache not set)
|
|
||||||
if (not $no_cache) {
|
|
||||||
let nickel_cache = (lookup-nickel-cache $file_path)
|
|
||||||
|
|
||||||
if ($nickel_cache.valid? | default false) {
|
|
||||||
if $debug {
|
|
||||||
print $"✅ Cache hit: Nickel ($file_path)"
|
|
||||||
}
|
|
||||||
return $nickel_cache.data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Evaluate Nickel file (produces JSON output)
|
|
||||||
# Use 'nickel export' for both package-based and standalone Nickel files
|
|
||||||
let file_dir = ($file_path | path dirname)
|
|
||||||
let file_name = ($file_path | path basename)
|
|
||||||
let decl_mod_exists = (($file_dir | path join "nickel.mod") | path exists)
|
|
||||||
|
|
||||||
let result = if $decl_mod_exists {
|
|
||||||
# Use 'nickel export' for package-based configs (SST pattern with nickel.mod)
|
|
||||||
# Must run from the config directory so relative paths in nickel.mod resolve correctly
|
|
||||||
(^sh -c $"cd '($file_dir)' && nickel export ($file_name) --format json" | complete)
|
|
||||||
} else {
|
|
||||||
# Use 'nickel export' for standalone configs
|
|
||||||
(^nickel export $file_path --format json | complete)
|
|
||||||
}
|
|
||||||
|
|
||||||
let decl_output = $result.stdout
|
|
||||||
|
|
||||||
# Check if output is empty
|
|
||||||
if ($decl_output | is-empty) {
|
|
||||||
# Nickel compilation failed - return empty to trigger fallback to YAML
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Nickel config compilation failed, fallback to YAML will be used"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse JSON output (Nickel outputs JSON when --format json is specified)
|
|
||||||
let parsed = (do -i { $decl_output | from json })
|
|
||||||
|
|
||||||
if ($parsed | is-empty) or ($parsed | type) != "record" {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Failed to parse Nickel output as JSON"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract workspace_config key if it exists (Nickel wraps output in variable name)
|
|
||||||
let config = if (($parsed | columns) | any { |col| $col == "workspace_config" }) {
|
|
||||||
$parsed.workspace_config
|
|
||||||
} else {
|
|
||||||
$parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug {
|
|
||||||
print $"✅ Loaded Nickel config from ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cache the compiled Nickel output (if cache enabled and --no-cache not set)
|
|
||||||
if (not $no_cache) and ($config | type) == "record" {
|
|
||||||
cache-nickel-compile $file_path $config
|
|
||||||
}
|
|
||||||
|
|
||||||
$config
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deep merge two configuration records (right takes precedence)
|
|
||||||
export def deep-merge [
|
|
||||||
base: record
|
|
||||||
override: record
|
|
||||||
] {
|
|
||||||
mut result = $base
|
|
||||||
|
|
||||||
for key in ($override | columns) {
|
|
||||||
let override_value = ($override | get $key)
|
|
||||||
let base_value = ($base | get -o $key | default null)
|
|
||||||
|
|
||||||
if ($base_value | is-empty) {
|
|
||||||
# Key doesn't exist in base, add it
|
|
||||||
$result = ($result | insert $key $override_value)
|
|
||||||
} else if (($base_value | describe) == "record") and (($override_value | describe) == "record") {
|
|
||||||
# Both are records, merge recursively
|
|
||||||
$result = ($result | upsert $key (deep-merge $base_value $override_value))
|
|
||||||
} else {
|
|
||||||
# Override the value
|
|
||||||
$result = ($result | upsert $key $override_value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$result
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get a nested configuration value using dot notation
|
|
||||||
export def get-config-value [
|
|
||||||
config: record
|
|
||||||
path: string
|
|
||||||
default_value: any = null
|
|
||||||
] {
|
|
||||||
let path_parts = ($path | split row ".")
|
|
||||||
mut current = $config
|
|
||||||
|
|
||||||
for part in $path_parts {
|
|
||||||
let immutable_current = $current
|
|
||||||
let next_value = ($immutable_current | get -o $part | default null)
|
|
||||||
if ($next_value | is-empty) {
|
|
||||||
return $default_value
|
|
||||||
}
|
|
||||||
$current = $next_value
|
|
||||||
}
|
|
||||||
|
|
||||||
$current
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to create directory structure for user config
|
|
||||||
export def init-user-config [
|
|
||||||
--template: string = "user" # Template type: user, dev, prod, test
|
|
||||||
--force = false # Overwrite existing config
|
|
||||||
] {
|
|
||||||
let config_dir = ($env.HOME | path join ".config" | path join "provisioning")
|
|
||||||
|
|
||||||
if not ($config_dir | path exists) {
|
|
||||||
mkdir $config_dir
|
|
||||||
print $"Created user config directory: ($config_dir)"
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_config_path = ($config_dir | path join "config.toml")
|
|
||||||
|
|
||||||
# Determine template file based on template parameter
|
|
||||||
let template_file = match $template {
|
|
||||||
"user" => "config.user.toml.example"
|
|
||||||
"dev" => "config.dev.toml.example"
|
|
||||||
"prod" => "config.prod.toml.example"
|
|
||||||
"test" => "config.test.toml.example"
|
|
||||||
_ => {
|
|
||||||
print $"❌ Unknown template: ($template). Valid options: user, dev, prod, test"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find the template file in the project
|
|
||||||
let project_root = (get-project-root)
|
|
||||||
let template_path = ($project_root | path join $template_file)
|
|
||||||
|
|
||||||
if not ($template_path | path exists) {
|
|
||||||
print $"❌ Template file not found: ($template_path)"
|
|
||||||
print "Available templates should be in the project root directory"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if config already exists
|
|
||||||
if ($user_config_path | path exists) and not $force {
|
|
||||||
print $"⚠️ User config already exists: ($user_config_path)"
|
|
||||||
print "Use --force to overwrite or choose a different template"
|
|
||||||
print $"Current template: ($template)"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy template to user config
|
|
||||||
cp $template_path $user_config_path
|
|
||||||
print $"✅ Created user config from ($template) template: ($user_config_path)"
|
|
||||||
print ""
|
|
||||||
print "📝 Next steps:"
|
|
||||||
print $" 1. Edit the config file: ($user_config_path)"
|
|
||||||
print " 2. Update paths.base to point to your provisioning installation"
|
|
||||||
print " 3. Configure your preferred providers and settings"
|
|
||||||
print " 4. Test the configuration: ./core/nulib/provisioning validate config"
|
|
||||||
print ""
|
|
||||||
print $"💡 Template used: ($template_file)"
|
|
||||||
|
|
||||||
# Show template-specific guidance
|
|
||||||
match $template {
|
|
||||||
"dev" => {
|
|
||||||
print "🔧 Development template configured with:"
|
|
||||||
print " • Enhanced debugging enabled"
|
|
||||||
print " • Local provider as default"
|
|
||||||
print " • JSON output format"
|
|
||||||
print " • Check mode enabled by default"
|
|
||||||
}
|
|
||||||
"prod" => {
|
|
||||||
print "🏭 Production template configured with:"
|
|
||||||
print " • Minimal logging for security"
|
|
||||||
print " • AWS provider as default"
|
|
||||||
print " • Strict validation enabled"
|
|
||||||
print " • Backup and monitoring settings"
|
|
||||||
}
|
|
||||||
"test" => {
|
|
||||||
print "🧪 Testing template configured with:"
|
|
||||||
print " • Mock providers and safe defaults"
|
|
||||||
print " • Test isolation settings"
|
|
||||||
print " • CI/CD friendly configurations"
|
|
||||||
print " • Automatic cleanup enabled"
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
print "👤 User template configured with:"
|
|
||||||
print " • Balanced settings for general use"
|
|
||||||
print " • Comprehensive documentation"
|
|
||||||
print " • Safe defaults for all scenarios"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load environment configurations from Nickel schema
|
|
||||||
# Per ADR-003: Nickel as Source of Truth for all configuration
|
|
||||||
def load-environments-from-nickel [] {
|
|
||||||
let project_root = (get-project-root)
|
|
||||||
let environments_ncl = ($project_root | path join "provisioning" "schemas" "config" "environments" "main.ncl")
|
|
||||||
|
|
||||||
if not ($environments_ncl | path exists) {
|
|
||||||
# Fallback: return empty if Nickel file doesn't exist
|
|
||||||
# Loader will then try to use config.defaults.toml if available
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Export Nickel to JSON and parse
|
|
||||||
let export_result = (do {
|
|
||||||
nickel export --format json $environments_ncl
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $export_result.exit_code != 0 {
|
|
||||||
# If Nickel export fails, fallback gracefully
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse JSON output
|
|
||||||
$export_result.stdout | from json
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper function to get project root directory
|
|
||||||
def get-project-root [] {
|
|
||||||
# Try to find project root by looking for key files
|
|
||||||
let potential_roots = [
|
|
||||||
$env.PWD
|
|
||||||
($env.PWD | path dirname)
|
|
||||||
($env.PWD | path dirname | path dirname)
|
|
||||||
($env.PWD | path dirname | path dirname | path dirname)
|
|
||||||
($env.PWD | path dirname | path dirname | path dirname | path dirname)
|
|
||||||
]
|
|
||||||
|
|
||||||
for root in $potential_roots {
|
|
||||||
# Check for provisioning project indicators
|
|
||||||
if (($root | path join "config.defaults.toml" | path exists) or
|
|
||||||
($root | path join "nickel.mod" | path exists) or
|
|
||||||
($root | path join "core" "nulib" "provisioning" | path exists)) {
|
|
||||||
return $root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fallback to current directory
|
|
||||||
$env.PWD
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
nulib/lib_provisioning/config/loader/dag.nu
Normal file
58
nulib/lib_provisioning/config/loader/dag.nu
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use ../../workspace/notation.nu [get-workspace-path]
|
||||||
|
use ../../utils/nickel_processor.nu [ncl-eval]
|
||||||
|
|
||||||
|
# Resolve the provisioning root directory for --import-path.
|
||||||
|
def dag-provisioning-root [] : nothing -> string {
|
||||||
|
$env.PROVISIONING? | default "/usr/local/provisioning"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export a Nickel file and parse as JSON, returning Err on non-zero exit.
|
||||||
|
def dag-nickel-export [path: string] : nothing -> record {
|
||||||
|
let prov = (dag-provisioning-root)
|
||||||
|
ncl-eval $path [$prov]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load the DAG execution config for a workspace.
|
||||||
|
#
|
||||||
|
# Resolution order:
|
||||||
|
# 1. `provisioning/schemas/config/dag/main.ncl` — base defaults (execution, resolution, events)
|
||||||
|
# 2. `{workspace_root}/infra/{infra}/dag.ncl` — workspace composition; top-level keys override defaults
|
||||||
|
#
|
||||||
|
# The workspace dag.ncl is a WorkspaceComposition — it is intentionally included here so that
|
||||||
|
# workspace-level overrides to execution/resolution/events blocks (if present) propagate.
|
||||||
|
# If the workspace dag.ncl has no such keys, the merge is a no-op for those fields.
|
||||||
|
#
|
||||||
|
# Returns a record with at minimum: execution, resolution, events.
|
||||||
|
export def get-dag-config [
|
||||||
|
workspace?: string # Workspace name; if omitted uses PROVISIONING root defaults only
|
||||||
|
--infra (-i): string = "wuji" # Infra sub-directory name
|
||||||
|
] : nothing -> record {
|
||||||
|
let prov = (dag-provisioning-root)
|
||||||
|
let defaults_path = ($prov | path join "schemas" "config" "dag" "main.ncl")
|
||||||
|
|
||||||
|
if not ($defaults_path | path exists) {
|
||||||
|
error make { msg: $"dag config: defaults not found at ($defaults_path)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaults = (dag-nickel-export $defaults_path)
|
||||||
|
|
||||||
|
if ($workspace == null) or ($workspace | is-empty) {
|
||||||
|
return $defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws_root = (get-workspace-path $workspace)
|
||||||
|
if ($ws_root | is-empty) {
|
||||||
|
error make { msg: $"dag config: workspace '($workspace)' not found in registry" }
|
||||||
|
}
|
||||||
|
|
||||||
|
let dag_path = ($ws_root | path join "infra" $infra "dag.ncl")
|
||||||
|
if not ($dag_path | path exists) {
|
||||||
|
return $defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws_dag = (dag-nickel-export $dag_path)
|
||||||
|
|
||||||
|
# Shallow merge: workspace keys (execution, resolution, events) overwrite defaults at top level.
|
||||||
|
# Nu 0.110.0+ has no 'merge deep'; top-level block override is the correct granularity here.
|
||||||
|
$defaults | merge $ws_dag
|
||||||
|
}
|
||||||
|
|
@ -151,12 +151,14 @@ def set-config-value [
|
||||||
mut result = $current
|
mut result = $current
|
||||||
|
|
||||||
# Navigate to parent of target
|
# Navigate to parent of target
|
||||||
let parent_parts = ($path_parts | range 0 (($path_parts | length) - 1))
|
# Use drop instead of range for Nushell 0.109+ compatibility
|
||||||
|
let parent_parts = ($path_parts | drop)
|
||||||
let leaf_key = ($path_parts | last)
|
let leaf_key = ($path_parts | last)
|
||||||
|
|
||||||
for part in $parent_parts {
|
for part in $parent_parts {
|
||||||
if ($result | get -o $part | is-empty) {
|
# Use upsert instead of insert to avoid column_already_exists error
|
||||||
$result = ($result | insert $part {})
|
if ($result | get -o $part) == null {
|
||||||
|
$result = ($result | upsert $part {})
|
||||||
}
|
}
|
||||||
$current = ($result | get $part)
|
$current = ($result | get $part)
|
||||||
# Update parent in result would go here (mutable record limitation)
|
# Update parent in result would go here (mutable record limitation)
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,6 @@ export use ./environment.nu *
|
||||||
|
|
||||||
# Testing and interpolation utilities
|
# Testing and interpolation utilities
|
||||||
export use ./test.nu *
|
export use ./test.nu *
|
||||||
|
|
||||||
|
# DAG config accessor (execution, resolution, events defaults merged with workspace dag.ncl)
|
||||||
|
export use ./dag.nu *
|
||||||
|
|
|
||||||
|
|
@ -1,330 +0,0 @@
|
||||||
# File loader - Handles format detection and loading of config files
|
|
||||||
# NUSHELL 0.109 COMPLIANT - Using do-complete (Rule 5), each (Rule 8)
|
|
||||||
|
|
||||||
use ../helpers/merging.nu *
|
|
||||||
use ../cache/sops.nu *
|
|
||||||
|
|
||||||
# Load a configuration file with automatic format detection
|
|
||||||
# Supports: Nickel (.ncl), TOML (.toml), YAML (.yaml/.yml), JSON (.json)
|
|
||||||
export def load-config-file [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
format: string = "auto" # auto, ncl, yaml, toml, json
|
|
||||||
--no-cache = false
|
|
||||||
]: nothing -> record {
|
|
||||||
if not ($file_path | path exists) {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Required configuration file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Optional config file not found: ($file_path)"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug {
|
|
||||||
# log debug $"Loading config file: ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine format from file extension if auto
|
|
||||||
let file_format = if $format == "auto" {
|
|
||||||
let ext = ($file_path | path parse | get extension)
|
|
||||||
match $ext {
|
|
||||||
"ncl" => "ncl"
|
|
||||||
"k" => "nickel"
|
|
||||||
"yaml" | "yml" => "yaml"
|
|
||||||
"toml" => "toml"
|
|
||||||
"json" => "json"
|
|
||||||
_ => "toml" # default to toml
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$format
|
|
||||||
}
|
|
||||||
|
|
||||||
# Route to appropriate loader based on format
|
|
||||||
match $file_format {
|
|
||||||
"ncl" => (load-ncl-file $file_path $required $debug --no-cache $no_cache)
|
|
||||||
"nickel" => (load-nickel-file $file_path $required $debug --no-cache $no_cache)
|
|
||||||
"yaml" => (load-yaml-file $file_path $required $debug --no-cache $no_cache)
|
|
||||||
"toml" => (load-toml-file $file_path $required $debug)
|
|
||||||
"json" => (load-json-file $file_path $required $debug)
|
|
||||||
_ => (load-yaml-file $file_path $required $debug --no-cache $no_cache) # default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load NCL (Nickel) file using nickel export command
|
|
||||||
def load-ncl-file [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
--no-cache = false
|
|
||||||
]: nothing -> record {
|
|
||||||
# Check if Nickel compiler is available
|
|
||||||
let nickel_exists = (^which nickel | is-not-empty)
|
|
||||||
if not $nickel_exists {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Nickel compiler not found. Install from: https://nickel-lang.io/"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Nickel compiler not found, skipping: ($file_path)"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Evaluate Nickel file and export as JSON
|
|
||||||
let result = (do {
|
|
||||||
^nickel export --format json $file_path
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $result.exit_code == 0 {
|
|
||||||
do {
|
|
||||||
$result.stdout | from json
|
|
||||||
} | complete | if $in.exit_code == 0 { $in.stdout } else { {} }
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Failed to load Nickel config ($file_path): ($result.stderr)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Failed to load Nickel config: ($result.stderr)"
|
|
||||||
}
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load Nickel file (with cache support and nickel.mod handling)
|
|
||||||
def load-nickel-file [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
--no-cache = false
|
|
||||||
]: nothing -> record {
|
|
||||||
# Check if nickel command is available
|
|
||||||
let nickel_exists = (^which nickel | is-not-empty)
|
|
||||||
if not $nickel_exists {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Nickel compiler not found"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Evaluate Nickel file
|
|
||||||
let file_dir = ($file_path | path dirname)
|
|
||||||
let file_name = ($file_path | path basename)
|
|
||||||
let decl_mod_exists = (($file_dir | path join "nickel.mod") | path exists)
|
|
||||||
|
|
||||||
let result = if $decl_mod_exists {
|
|
||||||
# Use nickel export from config directory for package-based configs
|
|
||||||
(^sh -c $"cd '($file_dir)' && nickel export ($file_name) --format json" | complete)
|
|
||||||
} else {
|
|
||||||
# Use nickel export for standalone configs
|
|
||||||
(^nickel export $file_path --format json | complete)
|
|
||||||
}
|
|
||||||
|
|
||||||
let decl_output = $result.stdout
|
|
||||||
|
|
||||||
# Check if output is empty
|
|
||||||
if ($decl_output | is-empty) {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Nickel compilation failed"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse JSON output
|
|
||||||
let parsed = (do { $decl_output | from json } | complete)
|
|
||||||
|
|
||||||
if ($parsed.exit_code != 0) or ($parsed.stdout | is-empty) {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Failed to parse Nickel output"
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = $parsed.stdout
|
|
||||||
|
|
||||||
# Extract workspace_config key if it exists
|
|
||||||
let result_config = if (($config | columns) | any { |col| $col == "workspace_config" }) {
|
|
||||||
$config.workspace_config
|
|
||||||
} else {
|
|
||||||
$config
|
|
||||||
}
|
|
||||||
|
|
||||||
if $debug {
|
|
||||||
print $"✅ Loaded Nickel config from ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$result_config
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load YAML file with SOPS decryption support
|
|
||||||
def load-yaml-file [
|
|
||||||
file_path: string
|
|
||||||
required = false
|
|
||||||
debug = false
|
|
||||||
--no-cache = false
|
|
||||||
]: nothing -> record {
|
|
||||||
# Check if file is encrypted and auto-decrypt
|
|
||||||
if (check-if-sops-encrypted $file_path) {
|
|
||||||
if $debug {
|
|
||||||
print $"🔓 Detected encrypted SOPS file: ($file_path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try SOPS cache first (if cache enabled)
|
|
||||||
if (not $no_cache) {
|
|
||||||
let sops_cache = (lookup-sops-cache $file_path)
|
|
||||||
if ($sops_cache.valid? | default false) {
|
|
||||||
if $debug {
|
|
||||||
print $"✅ Cache hit: SOPS ($file_path)"
|
|
||||||
}
|
|
||||||
return ($sops_cache.data | from yaml)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Decrypt using SOPS
|
|
||||||
let decrypted_content = (decrypt-sops-file $file_path)
|
|
||||||
|
|
||||||
if ($decrypted_content | is-empty) {
|
|
||||||
if $debug {
|
|
||||||
print $"⚠️ Failed to decrypt, loading as plaintext"
|
|
||||||
}
|
|
||||||
do { open $file_path } | complete | if $in.exit_code == 0 { $in.stdout } else { {} }
|
|
||||||
} else {
|
|
||||||
# Cache decrypted content (if cache enabled)
|
|
||||||
if (not $no_cache) {
|
|
||||||
cache-sops-decrypt $file_path $decrypted_content
|
|
||||||
}
|
|
||||||
|
|
||||||
do { $decrypted_content | from yaml } | complete | if $in.exit_code == 0 { $in.stdout } else { {} }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Load unencrypted YAML file
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
do { open $file_path } | complete | if $in.exit_code == 0 { $in.stdout } else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Configuration file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Configuration file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load TOML file
|
|
||||||
def load-toml-file [file_path: string, required = false, debug = false]: nothing -> record {
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
do { open $file_path } | complete | if $in.exit_code == 0 { $in.stdout } else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Failed to load TOML file: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ TOML file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Load JSON file
|
|
||||||
def load-json-file [file_path: string, required = false, debug = false]: nothing -> record {
|
|
||||||
if ($file_path | path exists) {
|
|
||||||
do { open $file_path } | complete | if $in.exit_code == 0 { $in.stdout } else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ Failed to load JSON file: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if $required {
|
|
||||||
print $"❌ JSON file not found: ($file_path)"
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if a YAML/TOML file is encrypted with SOPS
|
|
||||||
def check-if-sops-encrypted [file_path: string]: nothing -> bool {
|
|
||||||
if not ($file_path | path exists) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_content = (do { open $file_path --raw } | complete)
|
|
||||||
|
|
||||||
if ($file_content.exit_code != 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for SOPS markers
|
|
||||||
if ($file_content.stdout | str contains "sops:") and ($file_content.stdout | str contains "ENC[") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
# Decrypt SOPS file
|
|
||||||
def decrypt-sops-file [file_path: string]: nothing -> string {
|
|
||||||
# Find SOPS config file
|
|
||||||
let sops_config = find-sops-config-path
|
|
||||||
|
|
||||||
# Decrypt using SOPS binary
|
|
||||||
let result = if ($sops_config | is-not-empty) {
|
|
||||||
(^sops --decrypt --config $sops_config $file_path | complete)
|
|
||||||
} else {
|
|
||||||
(^sops --decrypt $file_path | complete)
|
|
||||||
}
|
|
||||||
|
|
||||||
if $result.exit_code != 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
$result.stdout
|
|
||||||
}
|
|
||||||
|
|
||||||
# Find SOPS configuration file in standard locations
|
|
||||||
def find-sops-config-path []: nothing -> string {
|
|
||||||
let locations = [
|
|
||||||
".sops.yaml"
|
|
||||||
".sops.yml"
|
|
||||||
($env.PWD | path join ".sops.yaml")
|
|
||||||
($env.HOME | path join ".config" | path join "provisioning" | path join "sops.yaml")
|
|
||||||
]
|
|
||||||
|
|
||||||
# Use reduce --fold to find first existing location (Rule 3: no mutable variables)
|
|
||||||
$locations | reduce --fold "" {|loc, found|
|
|
||||||
if ($found | is-not-empty) {
|
|
||||||
$found
|
|
||||||
} else if ($loc | path exists) {
|
|
||||||
$loc
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -46,7 +46,7 @@ export def providers_list [
|
||||||
let configured_path = (get-providers-path)
|
let configured_path = (get-providers-path)
|
||||||
let providers_path = if ($configured_path | is-empty) {
|
let providers_path = if ($configured_path | is-empty) {
|
||||||
# Fallback to system providers directory
|
# Fallback to system providers directory
|
||||||
"/Users/Akasha/project-provisioning/provisioning/extensions/providers"
|
($env.PROVISIONING | path join "extensions/providers")
|
||||||
} else {
|
} else {
|
||||||
$configured_path
|
$configured_path
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
# Error handling: Result pattern (hybrid, no inline try-catch)
|
# Error handling: Result pattern (hybrid, no inline try-catch)
|
||||||
|
|
||||||
use lib_provisioning/result.nu *
|
use lib_provisioning/result.nu *
|
||||||
|
use ./utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
def main [--debug: bool = false, --region: string = "all"] {
|
def main [--debug: bool = false, --region: string = "all"] {
|
||||||
print "🌍 Multi-Region High Availability Deployment"
|
print "🌍 Multi-Region High Availability Deployment"
|
||||||
|
|
@ -111,7 +112,7 @@ def validate_environment [] {
|
||||||
|
|
||||||
# Validate Nickel configuration
|
# Validate Nickel configuration
|
||||||
print " Validating Nickel configuration..."
|
print " Validating Nickel configuration..."
|
||||||
let nickel_result = (try-wrap { nickel export workspace.ncl | from json | null })
|
let nickel_result = (ok (ncl-eval-soft "workspace.ncl" []))
|
||||||
|
|
||||||
if (is-err $nickel_result) {
|
if (is-err $nickel_result) {
|
||||||
error make {msg: $"Nickel validation failed: ($nickel_result.err)"}
|
error make {msg: $"Nickel validation failed: ($nickel_result.err)"}
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ def check-config-files [] {
|
||||||
status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" })
|
status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" })
|
||||||
issues: $issues
|
issues: $issues
|
||||||
recommendation: (if ($issues | is-not-empty) {
|
recommendation: (if ($issues | is-not-empty) {
|
||||||
"Review configuration files - See: docs/user/WORKSPACE_SWITCHING_GUIDE.md"
|
"Missing config files. Run: provisioning workspace init <name> to create workspace"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"All configuration files present"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,9 +85,9 @@ def check-workspace-structure [] {
|
||||||
status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" })
|
status: (if ($issues | is-empty) { "✅ Healthy" } else { "❌ Issues Found" })
|
||||||
issues: $issues
|
issues: $issues
|
||||||
recommendation: (if ($issues | is-not-empty) {
|
recommendation: (if ($issues | is-not-empty) {
|
||||||
"Initialize workspace structure - Run: provisioning workspace init"
|
"Workspace directories missing. Run: provisioning workspace init <name> to create structure"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"Workspace structure complete"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,9 +137,9 @@ def check-infrastructure-state [] {
|
||||||
})
|
})
|
||||||
issues: ($issues | append $warnings)
|
issues: ($issues | append $warnings)
|
||||||
recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) {
|
recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) {
|
||||||
"Review infrastructure definitions - See: docs/user/SERVICE_MANAGEMENT_GUIDE.md"
|
"No infrastructure defined. Run: provisioning generate infra --new <name> to create"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"Infrastructure configured"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,13 +150,12 @@ def check-platform-connectivity [] {
|
||||||
mut warnings = []
|
mut warnings = []
|
||||||
|
|
||||||
# Check orchestrator
|
# Check orchestrator
|
||||||
let orchestrator_port = config-get "orchestrator.port" 9090
|
let orchestrator_url = (config-get "platform.orchestrator.url" "http://localhost:9011")
|
||||||
|
|
||||||
do -i {
|
let orchestrator_response = (do -i {
|
||||||
http get $"http://localhost:($orchestrator_port)/health" --max-time 2sec e> /dev/null | ignore
|
http get $"($orchestrator_url)/health" --max-time 2sec
|
||||||
}
|
})
|
||||||
|
let orchestrator_healthy = ($orchestrator_response != null)
|
||||||
let orchestrator_healthy = ($env.LAST_EXIT_CODE? | default 1) == 0
|
|
||||||
|
|
||||||
if not $orchestrator_healthy {
|
if not $orchestrator_healthy {
|
||||||
$warnings = ($warnings | append "Orchestrator not responding - workflows will not be available")
|
$warnings = ($warnings | append "Orchestrator not responding - workflows will not be available")
|
||||||
|
|
@ -165,16 +164,34 @@ def check-platform-connectivity [] {
|
||||||
# Check control center
|
# Check control center
|
||||||
let control_center_port = config-get "control_center.port" 8080
|
let control_center_port = config-get "control_center.port" 8080
|
||||||
|
|
||||||
do -i {
|
let control_center_response = (do -i {
|
||||||
http get $"http://localhost:($control_center_port)/health" --max-time 1sec e> /dev/null | ignore
|
http get $"http://localhost:($control_center_port)/health" --max-time 1sec
|
||||||
}
|
})
|
||||||
|
let control_center_healthy = ($control_center_response != null)
|
||||||
let control_center_healthy = ($env.LAST_EXIT_CODE? | default 1) == 0
|
|
||||||
|
|
||||||
if not $control_center_healthy {
|
if not $control_center_healthy {
|
||||||
$warnings = ($warnings | append "Control Center not responding - web UI will not be available")
|
$warnings = ($warnings | append "Control Center not responding - web UI will not be available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build recommendation based on what's not running
|
||||||
|
let recommendation = if ($warnings | is-empty) {
|
||||||
|
"All services responding"
|
||||||
|
} else {
|
||||||
|
let not_running = []
|
||||||
|
let not_running = if not $orchestrator_healthy {
|
||||||
|
$not_running | append "orchestrator"
|
||||||
|
} else {
|
||||||
|
$not_running
|
||||||
|
}
|
||||||
|
let not_running = if not $control_center_healthy {
|
||||||
|
$not_running | append "control-center"
|
||||||
|
} else {
|
||||||
|
$not_running
|
||||||
|
}
|
||||||
|
|
||||||
|
$"Platform services not running: ($not_running | str join ', '). These services are optional for basic provisioning operations."
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
check: "Platform Services"
|
check: "Platform Services"
|
||||||
status: (if ($issues | is-empty) {
|
status: (if ($issues | is-empty) {
|
||||||
|
|
@ -183,11 +200,7 @@ def check-platform-connectivity [] {
|
||||||
"❌ Issues Found"
|
"❌ Issues Found"
|
||||||
})
|
})
|
||||||
issues: ($issues | append $warnings)
|
issues: ($issues | append $warnings)
|
||||||
recommendation: (if ($warnings | is-not-empty) {
|
recommendation: $recommendation
|
||||||
"Start platform services - See: .claude/features/orchestrator-architecture.md"
|
|
||||||
} else {
|
|
||||||
"No action needed"
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,9 +253,9 @@ def check-nickel-schemas [] {
|
||||||
})
|
})
|
||||||
issues: ($issues | append $warnings)
|
issues: ($issues | append $warnings)
|
||||||
recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) {
|
recommendation: (if ($issues | is-not-empty) or ($warnings | is-not-empty) {
|
||||||
"Review Nickel schemas - See: .claude/guidelines/nickel/"
|
"Nickel schemas missing. Ensure provisioning/schemas/ directory exists"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"Schemas validated"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -287,9 +300,9 @@ def check-security-config [] {
|
||||||
})
|
})
|
||||||
issues: ($issues | append $warnings)
|
issues: ($issues | append $warnings)
|
||||||
recommendation: (if ($warnings | is-not-empty) {
|
recommendation: (if ($warnings | is-not-empty) {
|
||||||
"Configure security features - See: docs/user/CONFIG_ENCRYPTION_GUIDE.md"
|
"Security optional. Install: brew install sops age (encryption tools)"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"Security configured"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -324,9 +337,9 @@ def check-provider-credentials [] {
|
||||||
})
|
})
|
||||||
issues: ($issues | append $warnings)
|
issues: ($issues | append $warnings)
|
||||||
recommendation: (if ($warnings | is-not-empty) {
|
recommendation: (if ($warnings | is-not-empty) {
|
||||||
"Configure provider credentials - See: docs/user/SERVICE_MANAGEMENT_GUIDE.md"
|
"Credentials not set. Export: UPCLOUD_USERNAME/PASSWORD or AWS_ACCESS_KEY_ID/SECRET"
|
||||||
} else {
|
} else {
|
||||||
"No action needed"
|
"Credentials configured"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,75 +7,72 @@ use ../user/config.nu *
|
||||||
|
|
||||||
# Determine current deployment phase
|
# Determine current deployment phase
|
||||||
def get-deployment-phase [] {
|
def get-deployment-phase [] {
|
||||||
let result = (do {
|
let user_config = load-user-config
|
||||||
let user_config = load-user-config
|
let active = ($user_config.active_workspace? | default null)
|
||||||
let active = ($user_config.active_workspace? | default null)
|
|
||||||
|
|
||||||
if $active == null {
|
if $active == null {
|
||||||
return "no_workspace"
|
return "no_workspace"
|
||||||
}
|
|
||||||
|
|
||||||
let workspace = ($user_config.workspaces | where name == $active | first)
|
|
||||||
let ws_path = ($workspace.path? | default "")
|
|
||||||
|
|
||||||
if not ($ws_path | path exists) {
|
|
||||||
return "invalid_workspace"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for infrastructure definitions
|
|
||||||
let infra_path = ($ws_path | path join "infra")
|
|
||||||
let has_infra = if ($infra_path | path exists) {
|
|
||||||
(ls $infra_path | where type == dir | length) > 0
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
if not $has_infra {
|
|
||||||
return "no_infrastructure"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for server state
|
|
||||||
let state_path = ($ws_path | path join "runtime" | path join "state")
|
|
||||||
let has_servers = if ($state_path | path exists) {
|
|
||||||
(ls $state_path --all | where name =~ r"server.*\.state$" | length) > 0
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
if not $has_servers {
|
|
||||||
return "no_servers"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for taskserv installations
|
|
||||||
let has_taskservs = if ($state_path | path exists) {
|
|
||||||
(ls $state_path --all | where name =~ r"taskserv.*\.state$" | length) > 0
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
if not $has_taskservs {
|
|
||||||
return "no_taskservs"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check for cluster deployments
|
|
||||||
let has_clusters = if ($state_path | path exists) {
|
|
||||||
(ls $state_path --all | where name =~ r"cluster.*\.state$" | length) > 0
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
if not $has_clusters {
|
|
||||||
return "no_clusters"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "deployed"
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $result.exit_code == 0 {
|
|
||||||
$result.stdout | str trim
|
|
||||||
} else {
|
|
||||||
"error"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let workspaces = ($user_config.workspaces | where name == $active)
|
||||||
|
if ($workspaces | length) == 0 {
|
||||||
|
return "invalid_workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = ($workspaces | first)
|
||||||
|
let ws_path = ($workspace.path? | default "")
|
||||||
|
|
||||||
|
if ($ws_path | is-empty) or not ($ws_path | path exists) {
|
||||||
|
return "invalid_workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for infrastructure definitions
|
||||||
|
let infra_path = ($ws_path | path join "infra")
|
||||||
|
let has_infra = if ($infra_path | path exists) {
|
||||||
|
(ls $infra_path | where type == dir | length) > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $has_infra {
|
||||||
|
return "no_infrastructure"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for server state
|
||||||
|
let state_path = ($ws_path | path join "runtime" | path join "state")
|
||||||
|
let has_servers = if ($state_path | path exists) {
|
||||||
|
(ls $state_path --all | where name =~ r"server.*\.state$" | length) > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $has_servers {
|
||||||
|
return "no_servers"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for taskserv installations
|
||||||
|
let has_taskservs = if ($state_path | path exists) {
|
||||||
|
(ls $state_path --all | where name =~ r"taskserv.*\.state$" | length) > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $has_taskservs {
|
||||||
|
return "no_taskservs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for cluster deployments
|
||||||
|
let has_clusters = if ($state_path | path exists) {
|
||||||
|
(ls $state_path --all | where name =~ r"cluster.*\.state$" | length) > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if not $has_clusters {
|
||||||
|
return "no_clusters"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "deployed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get next steps for no workspace phase
|
# Get next steps for no workspace phase
|
||||||
|
|
@ -241,7 +238,7 @@ def next-steps-error [] {
|
||||||
export def "provisioning next" [] {
|
export def "provisioning next" [] {
|
||||||
let phase = (get-deployment-phase)
|
let phase = (get-deployment-phase)
|
||||||
|
|
||||||
match $phase {
|
let message = match $phase {
|
||||||
"no_workspace" => { next-steps-no-workspace }
|
"no_workspace" => { next-steps-no-workspace }
|
||||||
"invalid_workspace" => { next-steps-no-workspace }
|
"invalid_workspace" => { next-steps-no-workspace }
|
||||||
"no_infrastructure" => { next-steps-no-infrastructure }
|
"no_infrastructure" => { next-steps-no-infrastructure }
|
||||||
|
|
@ -252,6 +249,8 @@ export def "provisioning next" [] {
|
||||||
"error" => { next-steps-error }
|
"error" => { next-steps-error }
|
||||||
_ => { next-steps-error }
|
_ => { next-steps-error }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print $message
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get current deployment phase (machine-readable)
|
# Get current deployment phase (machine-readable)
|
||||||
|
|
@ -266,6 +265,13 @@ export def "provisioning phase" [] {
|
||||||
description: "No workspace configured"
|
description: "No workspace configured"
|
||||||
ready_for_deployment: false
|
ready_for_deployment: false
|
||||||
}
|
}
|
||||||
|
"invalid_workspace" => {
|
||||||
|
phase: "initialization"
|
||||||
|
step: 1
|
||||||
|
total_steps: 5
|
||||||
|
description: "Workspace path invalid or missing"
|
||||||
|
ready_for_deployment: false
|
||||||
|
}
|
||||||
"no_infrastructure" => {
|
"no_infrastructure" => {
|
||||||
phase: "configuration"
|
phase: "configuration"
|
||||||
step: 2
|
step: 2
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ def check-nickel-installed [] {
|
||||||
let version_info = if $installed {
|
let version_info = if $installed {
|
||||||
let result = (do { ^nickel --version } | complete)
|
let result = (do { ^nickel --version } | complete)
|
||||||
if $result.exit_code == 0 {
|
if $result.exit_code == 0 {
|
||||||
$result.stdout | str trim
|
let version_full = ($result.stdout | str trim)
|
||||||
|
# Extract version number and revision: "X.Y.Z (rev ...)"
|
||||||
|
let version_short = ($version_full | str replace 'nickel-lang-cli nickel ' '')
|
||||||
|
$version_short
|
||||||
} else {
|
} else {
|
||||||
"unknown"
|
"unknown"
|
||||||
}
|
}
|
||||||
|
|
@ -61,31 +64,31 @@ def check-nickel-installed [] {
|
||||||
def check-plugins [] {
|
def check-plugins [] {
|
||||||
let required_plugins = [
|
let required_plugins = [
|
||||||
{
|
{
|
||||||
name: "nu_plugin_nickel"
|
name: "nickel"
|
||||||
description: "Nickel integration"
|
description: "Nickel integration"
|
||||||
optional: true
|
optional: true
|
||||||
docs: "docs/user/PLUGIN_INTEGRATION_GUIDE.md"
|
docs: "docs/user/PLUGIN_INTEGRATION_GUIDE.md"
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name: "nu_plugin_tera"
|
name: "tera"
|
||||||
description: "Template rendering"
|
description: "Template rendering"
|
||||||
optional: false
|
optional: false
|
||||||
docs: "docs/user/PLUGIN_INTEGRATION_GUIDE.md"
|
docs: "docs/user/PLUGIN_INTEGRATION_GUIDE.md"
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name: "nu_plugin_auth"
|
name: "auth"
|
||||||
description: "Authentication"
|
description: "Authentication"
|
||||||
optional: true
|
optional: true
|
||||||
docs: "docs/user/AUTHENTICATION_LAYER_GUIDE.md"
|
docs: "docs/user/AUTHENTICATION_LAYER_GUIDE.md"
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name: "nu_plugin_kms"
|
name: "kms"
|
||||||
description: "Key management"
|
description: "Key management"
|
||||||
optional: true
|
optional: true
|
||||||
docs: "docs/user/RUSTYVAULT_KMS_GUIDE.md"
|
docs: "docs/user/RUSTYVAULT_KMS_GUIDE.md"
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name: "nu_plugin_orchestrator"
|
name: "orchestrator"
|
||||||
description: "Orchestrator integration"
|
description: "Orchestrator integration"
|
||||||
optional: true
|
optional: true
|
||||||
docs: ".claude/features/orchestrator-architecture.md"
|
docs: ".claude/features/orchestrator-architecture.md"
|
||||||
|
|
@ -162,6 +165,12 @@ def check-providers [] {
|
||||||
let available_providers = if ($providers_path | path exists) {
|
let available_providers = if ($providers_path | path exists) {
|
||||||
ls $providers_path
|
ls $providers_path
|
||||||
| where type == dir
|
| where type == dir
|
||||||
|
| where { |item|
|
||||||
|
let provider_name = ($item.name | path basename)
|
||||||
|
let bin_install_sh = ($providers_path | path join $provider_name | path join "bin" | path join "install.sh" | path exists)
|
||||||
|
let bin_install_nu = ($providers_path | path join $provider_name | path join "bin" | path join "install.nu" | path exists)
|
||||||
|
$bin_install_sh or $bin_install_nu
|
||||||
|
}
|
||||||
| get name
|
| get name
|
||||||
| path basename
|
| path basename
|
||||||
| str join ", "
|
| str join ", "
|
||||||
|
|
@ -187,22 +196,21 @@ def check-providers [] {
|
||||||
|
|
||||||
# Check orchestrator service
|
# Check orchestrator service
|
||||||
def check-orchestrator [] {
|
def check-orchestrator [] {
|
||||||
let orchestrator_port = config-get "orchestrator.port" 9090
|
let orchestrator_url = (config-get "platform.orchestrator.url" "http://localhost:9011")
|
||||||
let orchestrator_host = config-get "orchestrator.host" "localhost"
|
|
||||||
|
|
||||||
# Try to ping orchestrator health endpoint (handle connection errors gracefully)
|
# Try to ping orchestrator health endpoint (handle connection errors gracefully)
|
||||||
let result = (do { ^curl -s -f $"http://($orchestrator_host):($orchestrator_port)/health" --max-time 2 } | complete)
|
let result = (do { ^curl -s -f $"($orchestrator_url)/health" --max-time 2 } | complete)
|
||||||
let is_running = ($result.exit_code == 0)
|
let is_running = ($result.exit_code == 0)
|
||||||
|
|
||||||
{
|
{
|
||||||
component: "Orchestrator Service"
|
component: "Orchestrator Service"
|
||||||
status: (if $is_running { "✅" } else { "⚠️" })
|
status: (if $is_running { "✅" } else { "⚠️" })
|
||||||
version: (if $is_running { $"running on :($orchestrator_port)" } else { "not running" })
|
version: (if $is_running { $"running on ($orchestrator_url)" } else { "not running" })
|
||||||
required: "recommended"
|
required: "recommended"
|
||||||
message: (if $is_running {
|
message: (if $is_running {
|
||||||
"Service healthy and responding"
|
"Service healthy and responding"
|
||||||
} else {
|
} else {
|
||||||
"Service not responding - start with: cd provisioning/platform/orchestrator && ./scripts/start-orchestrator.nu"
|
"Optional service not running. Review startup options"
|
||||||
})
|
})
|
||||||
docs: ".claude/features/orchestrator-architecture.md"
|
docs: ".claude/features/orchestrator-architecture.md"
|
||||||
}
|
}
|
||||||
|
|
@ -251,25 +259,18 @@ def check-platform-services [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Collect all status checks
|
# Collect all status checks
|
||||||
|
# Refactored to use immutable pattern per Rule 3 (Nushell 0.110.0 compatibility)
|
||||||
def get-all-checks [] {
|
def get-all-checks [] {
|
||||||
mut checks = []
|
# Concatenate all check results immutably
|
||||||
|
[
|
||||||
# Core requirements
|
(check-nushell-version)
|
||||||
$checks = ($checks | append (check-nushell-version))
|
(check-nickel-installed)
|
||||||
$checks = ($checks | append (check-nickel-installed))
|
(check-plugins)
|
||||||
|
(check-workspace)
|
||||||
# Plugins
|
(check-providers)
|
||||||
$checks = ($checks | append (check-plugins))
|
(check-orchestrator)
|
||||||
|
(check-platform-services)
|
||||||
# Configuration
|
] | flatten
|
||||||
$checks = ($checks | append (check-workspace))
|
|
||||||
$checks = ($checks | append (check-providers))
|
|
||||||
|
|
||||||
# Services
|
|
||||||
$checks = ($checks | append (check-orchestrator))
|
|
||||||
$checks = ($checks | append (check-platform-services))
|
|
||||||
|
|
||||||
$checks | flatten
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main system status command
|
# Main system status command
|
||||||
|
|
@ -278,7 +279,7 @@ export def "provisioning status" [] {
|
||||||
print $"(ansi cyan_bold)Provisioning Platform Status(ansi reset)\n"
|
print $"(ansi cyan_bold)Provisioning Platform Status(ansi reset)\n"
|
||||||
|
|
||||||
let all_checks = (get-all-checks)
|
let all_checks = (get-all-checks)
|
||||||
let results = ($all_checks | select component status version message docs)
|
let results = ($all_checks | select component status version message)
|
||||||
print ($results | table)
|
print ($results | table)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ export def main [
|
||||||
|
|
||||||
let test_dir = ($env.FILE_PWD)
|
let test_dir = ($env.FILE_PWD)
|
||||||
|
|
||||||
let mut passed = 0
|
mut passed = 0
|
||||||
let mut failed = 0
|
mut failed = 0
|
||||||
let mut skipped = 0
|
mut skipped = 0
|
||||||
|
|
||||||
# OCI Client Tests
|
# OCI Client Tests
|
||||||
if $suite == "all" or $suite == "oci" {
|
if $suite == "all" or $suite == "oci" {
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ def generate_issues_section [issues: list] {
|
||||||
mut section = ""
|
mut section = ""
|
||||||
|
|
||||||
for issue in $issues {
|
for issue in $issues {
|
||||||
let relative_path = ($issue.file | str replace --all "/Users/Akasha/repo-cnz/src/provisioning/" "" | str replace --all "/Users/Akasha/repo-cnz/" "")
|
let relative_path = ($issue.file | str replace --all "($env.HOME | path join "repo-cnz/src/provisioning")" "" | str replace --all "($env.HOME | path join "repo-cnz")" "")
|
||||||
|
|
||||||
$section = $section + $"### ($issue.rule_id): ($issue.message)\n\n"
|
$section = $section + $"### ($issue.rule_id): ($issue.message)\n\n"
|
||||||
$section = $section + $"**File:** `($relative_path)`\n"
|
$section = $section + $"**File:** `($relative_path)`\n"
|
||||||
|
|
|
||||||
|
|
@ -361,7 +361,7 @@ export def orchestrate-from-iac [
|
||||||
let detector_bin = if ($env.PROVISIONING? | is-not-empty) {
|
let detector_bin = if ($env.PROVISIONING? | is-not-empty) {
|
||||||
$env.PROVISIONING | path join "platform" "target" "release" "provisioning-detector"
|
$env.PROVISIONING | path join "platform" "target" "release" "provisioning-detector"
|
||||||
} else {
|
} else {
|
||||||
"/Users/Akasha/project-provisioning/provisioning/platform/target/release/provisioning-detector"
|
($env.HOME | path join "project-provisioning/provisioning/platform/target/release/provisioning-detector")
|
||||||
}
|
}
|
||||||
|
|
||||||
let detect_result = (^$detector_bin detect $project_path --format json out+err>| complete)
|
let detect_result = (^$detector_bin detect $project_path --format json out+err>| complete)
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ export def kms-encrypt [
|
||||||
})
|
})
|
||||||
|
|
||||||
if $result != null {
|
if $result != null {
|
||||||
if ($result | describe) == "record" and "ciphertext" in $result {
|
if (($result | describe) | str starts-with "record") and "ciphertext" in $result {
|
||||||
return $result.ciphertext
|
return $result.ciphertext
|
||||||
} else if ($result | describe) == "string" {
|
} else if (($result | describe) | str starts-with "string") {
|
||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@ export def run_cmd_kms [
|
||||||
})
|
})
|
||||||
|
|
||||||
if $result != null {
|
if $result != null {
|
||||||
if ($result | describe) == "record" and "ciphertext" in $result {
|
if (($result | describe) | str starts-with "record") and "ciphertext" in $result {
|
||||||
return $result.ciphertext
|
return $result.ciphertext
|
||||||
} else if ($result | describe) == "string" {
|
} else if (($result | describe) | str starts-with "string") {
|
||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export def "discover-nickel-modules" [
|
||||||
] {
|
] {
|
||||||
# Fast path: don't load config, just use extensions path directly
|
# Fast path: don't load config, just use extensions path directly
|
||||||
# This avoids Nickel evaluation which can hang the system
|
# This avoids Nickel evaluation which can hang the system
|
||||||
let proj_root = ($env.PROVISIONING_ROOT? | default "/Users/Akasha/project-provisioning")
|
let proj_root = ($env.PROVISIONING_ROOT? | default ($env.HOME | path join "project-provisioning"))
|
||||||
let base_path = ($proj_root | path join "provisioning" "extensions" $type)
|
let base_path = ($proj_root | path join "provisioning" "extensions" $type)
|
||||||
|
|
||||||
if not ($base_path | path exists) {
|
if not ($base_path | path exists) {
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,12 @@ def download-oci-layers [
|
||||||
log-debug $"Downloading layer: ($layer.digest)"
|
log-debug $"Downloading layer: ($layer.digest)"
|
||||||
|
|
||||||
# Download blob using run-external
|
# Download blob using run-external
|
||||||
mut curl_args = ["-L" "-o" $layer_file $blob_url]
|
# Build curl args immutably per Rule 3
|
||||||
|
let base_args = ["-L" "-o" $layer_file $blob_url]
|
||||||
if ($auth_token | is-not-empty) {
|
let curl_args = if ($auth_token | is-not-empty) {
|
||||||
$curl_args = (["-H" $"Authorization: Bearer ($auth_token)"] | append $curl_args)
|
["-H" $"Authorization: Bearer ($auth_token)"] | append $base_args
|
||||||
|
} else {
|
||||||
|
$base_args
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = (do { ^curl ...$curl_args } | complete)
|
let result = (do { ^curl ...$curl_args } | complete)
|
||||||
|
|
@ -159,11 +161,12 @@ export def oci-push-artifact [
|
||||||
|
|
||||||
log-debug $"Uploading blob to ($blob_url)"
|
log-debug $"Uploading blob to ($blob_url)"
|
||||||
|
|
||||||
# Start upload using run-external
|
# Start upload using run-external - build args immutably per Rule 3
|
||||||
mut upload_start_args = ["-X" "POST" $blob_url]
|
let base_start_args = ["-X" "POST" $blob_url]
|
||||||
|
let upload_start_args = if ($auth_token | is-not-empty) {
|
||||||
if ($auth_token | is-not-empty) {
|
["-H" $"Authorization: Bearer ($auth_token)"] | append $base_start_args
|
||||||
$upload_start_args = (["-H" $"Authorization: Bearer ($auth_token)"] | append $upload_start_args)
|
} else {
|
||||||
|
$base_start_args
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_upload = (do {
|
let start_upload = (do {
|
||||||
|
|
@ -179,19 +182,20 @@ export def oci-push-artifact [
|
||||||
# Extract upload URL from Location header
|
# Extract upload URL from Location header
|
||||||
let upload_url = ($start_upload.stdout | str trim)
|
let upload_url = ($start_upload.stdout | str trim)
|
||||||
|
|
||||||
# Upload blob using run-external
|
# Upload blob using run-external - build args immutably per Rule 3
|
||||||
mut upload_args = ["-X" "PUT"]
|
let auth_headers = if ($auth_token | is-not-empty) {
|
||||||
|
["-H" $"Authorization: Bearer ($auth_token)"]
|
||||||
if ($auth_token | is-not-empty) {
|
} else {
|
||||||
$upload_args = ($upload_args | append "-H")
|
[]
|
||||||
$upload_args = ($upload_args | append $"Authorization: Bearer ($auth_token)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$upload_args = ($upload_args | append "-H")
|
let upload_args = [
|
||||||
$upload_args = ($upload_args | append "Content-Type: application/octet-stream")
|
"-X" "PUT"
|
||||||
$upload_args = ($upload_args | append "--data-binary")
|
] | append $auth_headers | append [
|
||||||
$upload_args = ($upload_args | append $"@($temp_tarball)")
|
"-H" "Content-Type: application/octet-stream"
|
||||||
$upload_args = ($upload_args | append $"($upload_url)?digest=($blob_digest)")
|
"--data-binary" $"@($temp_tarball)"
|
||||||
|
$"($upload_url)?digest=($blob_digest)"
|
||||||
|
]
|
||||||
|
|
||||||
let upload_result = (do { ^curl ...$upload_args } | complete)
|
let upload_result = (do { ^curl ...$upload_args } | complete)
|
||||||
|
|
||||||
|
|
@ -235,19 +239,20 @@ export def oci-push-artifact [
|
||||||
|
|
||||||
log-debug $"Uploading manifest to ($manifest_url)"
|
log-debug $"Uploading manifest to ($manifest_url)"
|
||||||
|
|
||||||
# Upload manifest using run-external
|
# Upload manifest using run-external - build args immutably per Rule 3
|
||||||
mut manifest_args = ["-X" "PUT"]
|
let auth_headers = if ($auth_token | is-not-empty) {
|
||||||
|
["-H" $"Authorization: Bearer ($auth_token)"]
|
||||||
if ($auth_token | is-not-empty) {
|
} else {
|
||||||
$manifest_args = ($manifest_args | append "-H")
|
[]
|
||||||
$manifest_args = ($manifest_args | append $"Authorization: Bearer ($auth_token)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$manifest_args = ($manifest_args | append "-H")
|
let manifest_args = [
|
||||||
$manifest_args = ($manifest_args | append "Content-Type: application/vnd.oci.image.manifest.v1+json")
|
"-X" "PUT"
|
||||||
$manifest_args = ($manifest_args | append "-d")
|
] | append $auth_headers | append [
|
||||||
$manifest_args = ($manifest_args | append $manifest_json)
|
"-H" "Content-Type: application/vnd.oci.image.manifest.v1+json"
|
||||||
$manifest_args = ($manifest_args | append $manifest_url)
|
"-d" $manifest_json
|
||||||
|
$manifest_url
|
||||||
|
]
|
||||||
|
|
||||||
let manifest_result = (do { ^curl ...$manifest_args } | complete)
|
let manifest_result = (do { ^curl ...$manifest_args } | complete)
|
||||||
|
|
||||||
|
|
@ -426,15 +431,14 @@ export def oci-delete-artifact [
|
||||||
# Delete manifest
|
# Delete manifest
|
||||||
let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($digest)"
|
let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($digest)"
|
||||||
|
|
||||||
# Delete using run-external
|
# Delete using run-external - build args immutably per Rule 3
|
||||||
mut delete_args = ["-X" "DELETE"]
|
let auth_headers = if ($auth_token | is-not-empty) {
|
||||||
|
["-H" $"Authorization: Bearer ($auth_token)"]
|
||||||
if ($auth_token | is-not-empty) {
|
} else {
|
||||||
$delete_args = ($delete_args | append "-H")
|
[]
|
||||||
$delete_args = ($delete_args | append $"Authorization: Bearer ($auth_token)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$delete_args = ($delete_args | append $manifest_url)
|
let delete_args = ["-X" "DELETE"] | append $auth_headers | append $manifest_url
|
||||||
|
|
||||||
let delete_result = (do { ^curl ...$delete_args } | complete)
|
let delete_result = (do { ^curl ...$delete_args } | complete)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,121 @@
|
||||||
# Platform Service Auto-Start
|
# Platform Service Auto-Start
|
||||||
# Manages automatic startup of platform services
|
|
||||||
|
|
||||||
use target.nu *
|
use target.nu *
|
||||||
use health.nu *
|
use health.nu *
|
||||||
|
|
||||||
# Start a platform service (stub - actual implementation depends on deployment mode)
|
# Get binary name from service name
|
||||||
|
def get-binary-name [service: string] {
|
||||||
|
let name = ($service | str replace "_" "-")
|
||||||
|
$"provisioning-($name)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get config directory for service
|
||||||
|
def get-service-config-dir [] {
|
||||||
|
if ($nu.os-info.name == "macos") {
|
||||||
|
$"($env.HOME)/Library/Application Support/provisioning/platform"
|
||||||
|
} else {
|
||||||
|
$"($env.HOME)/.config/provisioning/platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build environment variables for service
|
||||||
|
def build-service-env [service: string] {
|
||||||
|
let cfg_dir = (get-service-config-dir)
|
||||||
|
let base_env = {RUST_LOG: "info"}
|
||||||
|
|
||||||
|
match $service {
|
||||||
|
"orchestrator" => {
|
||||||
|
$base_env
|
||||||
|
| insert PROVISIONING_CONFIG_DIR $cfg_dir
|
||||||
|
| insert ORCHESTRATOR_MODE "local"
|
||||||
|
}
|
||||||
|
"vault_service" => {
|
||||||
|
$base_env
|
||||||
|
| insert PROVISIONING_CONFIG_DIR $cfg_dir
|
||||||
|
| insert VAULT_SERVICE_MODE "local"
|
||||||
|
}
|
||||||
|
"control_center" => {
|
||||||
|
$base_env
|
||||||
|
| insert PROVISIONING_CONFIG_DIR $cfg_dir
|
||||||
|
| insert CONTROL_CENTER_MODE "local"
|
||||||
|
}
|
||||||
|
"ai_service" => {
|
||||||
|
$base_env
|
||||||
|
| insert PROVISIONING_CONFIG_DIR $cfg_dir
|
||||||
|
| insert AI_SERVICE_MODE "local"
|
||||||
|
}
|
||||||
|
"extension_registry" => {
|
||||||
|
$base_env
|
||||||
|
| insert PROVISIONING_CONFIG_DIR $cfg_dir
|
||||||
|
| insert EXTENSION_REGISTRY_MODE "local"
|
||||||
|
}
|
||||||
|
_ => $base_env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start a platform service
|
||||||
export def start-service [service: string] {
|
export def start-service [service: string] {
|
||||||
let config = (get-platform-service-config $service)
|
let config = (get-deployment-service-config $service)
|
||||||
|
let enabled = ($config.enabled? | default false)
|
||||||
|
|
||||||
print $"Starting service: ($service)"
|
if not $enabled {
|
||||||
print $" Endpoint: ($config.endpoint)"
|
print $"⊘ ($service) is disabled in deployment-mode.ncl"
|
||||||
print $" Mode: ($config.deployment_mode)"
|
return false
|
||||||
print $" Note: Auto-start implementation depends on actual service deployment"
|
}
|
||||||
|
|
||||||
# In a real implementation, this would:
|
if (check-service-health $service) {
|
||||||
# - For 'binary' mode: Start the binary directly
|
print $"✓ ($service) is already running"
|
||||||
# - For 'docker' mode: Start docker container
|
return true
|
||||||
# - For 'systemd' mode: Use systemctl start
|
}
|
||||||
# - For 'remote' mode: Skip (remote service management)
|
|
||||||
|
let port = (
|
||||||
|
if (($config.server?) != null) {
|
||||||
|
$config.server.port
|
||||||
|
} else {
|
||||||
|
$config.port? | default null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let binary_name = (get-binary-name $service)
|
||||||
|
let binary_path = $"($env.HOME)/.local/bin/($binary_name)"
|
||||||
|
|
||||||
|
if not ($binary_path | path exists) {
|
||||||
|
print $"✗ Binary not found: ($binary_path)"
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_dir = $"($env.HOME)/.provisioning/logs"
|
||||||
|
^mkdir -p $log_dir
|
||||||
|
|
||||||
|
let log_file = $"($log_dir)/($service).log"
|
||||||
|
let env_vars = (build-service-env $service)
|
||||||
|
|
||||||
|
print $"→ Starting ($service) on port ($port)..."
|
||||||
|
|
||||||
|
let log_dir_expanded = ($log_dir | path expand)
|
||||||
|
^mkdir -p $log_dir_expanded
|
||||||
|
|
||||||
|
let cfg_dir = (get-service-config-dir)
|
||||||
|
let log_expanded = ($log_file | path expand)
|
||||||
|
let start_cmd = $"env RUST_LOG=info PROVISIONING_CONFIG_DIR='($cfg_dir)' '($binary_path)' > '($log_expanded)' 2>&1 &"
|
||||||
|
|
||||||
|
# Execute the command via shell to handle background execution and redirections
|
||||||
|
^sh -c $start_cmd
|
||||||
|
|
||||||
|
sleep 2sec
|
||||||
|
|
||||||
|
if (check-service-health $service) {
|
||||||
|
print $"✓ ($service) started on port ($port)"
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
print $"✗ ($service) failed to start - check logs at ($log_file)"
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop a platform service
|
# Stop a platform service
|
||||||
export def stop-service [service: string] {
|
export def stop-service [service: string] {
|
||||||
let config = (get-platform-service-config $service)
|
|
||||||
|
|
||||||
print $"Stopping service: ($service)"
|
print $"Stopping service: ($service)"
|
||||||
print $" Note: Stop implementation depends on actual service deployment"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Restart a platform service
|
# Restart a platform service
|
||||||
|
|
@ -35,42 +125,59 @@ export def restart-service [service: string] {
|
||||||
start-service $service
|
start-service $service
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start all required services
|
# Start all enabled services
|
||||||
export def start-required-services [] {
|
export def start-required-services [] {
|
||||||
let required = (list-required-platform-services)
|
let enabled_services = (get-enabled-services)
|
||||||
|
|
||||||
$required | each {|item|
|
if ($enabled_services | is-empty) {
|
||||||
if not (check-service-health $item.name) {
|
print "⊘ No services enabled in deployment-mode.ncl"
|
||||||
start-service $item.name
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = ($enabled_services | length)
|
||||||
|
print $"Starting ($count) enabled service\(s\)..."
|
||||||
|
print ""
|
||||||
|
|
||||||
|
let failed = (
|
||||||
|
$enabled_services | reduce --fold [] {|item, acc|
|
||||||
|
let service = $item.name
|
||||||
|
if (start-service $service) {
|
||||||
|
$acc
|
||||||
|
} else {
|
||||||
|
$acc | append $service
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
print ""
|
||||||
|
if (($failed | length) > 0) {
|
||||||
|
let fail_count = ($failed | length)
|
||||||
|
print $"⚠ ($fail_count) service\(s\) failed to start:"
|
||||||
|
$failed | each {|svc|
|
||||||
|
print $" - ($svc)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print "✓ All enabled services started successfully"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get status of all services
|
# Get status of all services
|
||||||
export def get-service-status [] {
|
export def get-service-status [] {
|
||||||
let services = (list-services)
|
get-enabled-services | each {|item|
|
||||||
|
let healthy = (check-service-health $item.name)
|
||||||
mut result = []
|
{
|
||||||
for svc in $services {
|
service: $item.name
|
||||||
let healthy = (check-service-health $svc)
|
|
||||||
$result = ($result | append {
|
|
||||||
service: $svc
|
|
||||||
status: (if $healthy { "running" } else { "stopped" })
|
status: (if $healthy { "running" } else { "stopped" })
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
$result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enable auto-start for a service
|
# Enable auto-start
|
||||||
export def enable-autostart [service: string] {
|
export def enable-autostart [service: string] {
|
||||||
# This would update the platform configuration
|
|
||||||
# to set auto_start: true for the service
|
|
||||||
print $"Enabled auto-start for: ($service)"
|
print $"Enabled auto-start for: ($service)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Disable auto-start for a service
|
# Disable auto-start
|
||||||
export def disable-autostart [service: string] {
|
export def disable-autostart [service: string] {
|
||||||
# This would update the platform configuration
|
|
||||||
# to set auto_start: false for the service
|
|
||||||
print $"Disabled auto-start for: ($service)"
|
print $"Disabled auto-start for: ($service)"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@
|
||||||
# Infrastructure-agnostic: supports Docker, Kubernetes, remote servers, etc.
|
# Infrastructure-agnostic: supports Docker, Kubernetes, remote servers, etc.
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
use ../config/context_manager.nu [get-active-workspace]
|
||||||
|
use ../setup/mod.nu [get-config-base-path]
|
||||||
use ../utils/logging.nu *
|
use ../utils/logging.nu *
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
use ../services/health.nu *
|
use ../services/health.nu *
|
||||||
use ../services/lifecycle.nu *
|
use ../services/lifecycle.nu *
|
||||||
use ../services/dependencies.nu *
|
use ../services/dependencies.nu *
|
||||||
|
|
@ -21,50 +24,63 @@ def get-service-config [service_name: string] {
|
||||||
# Get deployment configuration from workspace
|
# Get deployment configuration from workspace
|
||||||
def get-deployment-config [] {
|
def get-deployment-config [] {
|
||||||
# Try to load workspace-specific deployment config
|
# Try to load workspace-specific deployment config
|
||||||
let workspace_config_path = (get-workspace-path | path join "config" "platform" "deployment.toml")
|
let workspace = (get-active-workspace)
|
||||||
|
|
||||||
if ($workspace_config_path | path exists) {
|
if ($workspace != null) {
|
||||||
open $workspace_config_path
|
let workspace_config_path = ($workspace.path | path join "config" "platform" "deployment.toml")
|
||||||
} else {
|
|
||||||
# Fallback to global config
|
if ($workspace_config_path | path exists) {
|
||||||
{
|
return (open $workspace_config_path)
|
||||||
deployment: {
|
}
|
||||||
mode: (config-get "platform.deployment.mode" "docker-compose")
|
}
|
||||||
location_type: (config-get "platform.deployment.location.type" "local")
|
|
||||||
|
# Try to load platform deployment mode configuration (Nickel)
|
||||||
|
let config_base = (get-config-base-path)
|
||||||
|
let deployment_ncl = ($config_base | path join "platform" "deployment-mode.ncl")
|
||||||
|
|
||||||
|
if ($deployment_ncl | path exists) {
|
||||||
|
let content = (ncl-eval-soft $deployment_ncl [($env.PROVISIONING? | default "/usr/local/provisioning")] null)
|
||||||
|
if $content != null {
|
||||||
|
let deployment_mode = ($content.mode? | default "local")
|
||||||
|
return {
|
||||||
|
deployment: {
|
||||||
|
mode: $deployment_mode
|
||||||
|
location_type: (if $deployment_mode == "local" { "local" } else { "remote" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Final fallback to defaults
|
||||||
|
{
|
||||||
|
deployment: {
|
||||||
|
mode: "local"
|
||||||
|
location_type: "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get deployment mode from configuration
|
# Get deployment mode from configuration
|
||||||
def get-deployment-mode [] {
|
def get-deployment-mode [] {
|
||||||
let config = (get-deployment-config)
|
let config = (get-deployment-config)
|
||||||
$config.deployment.mode? | default "docker-compose"
|
$config.deployment.mode? | default "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get platform services deployment location
|
# Get platform services deployment location
|
||||||
def get-deployment-location [] {
|
def get-deployment-location [] {
|
||||||
let config = (get-deployment-config)
|
let config = (get-deployment-config)
|
||||||
$config.deployment? | default {
|
$config.deployment? | default {
|
||||||
mode: "docker-compose"
|
mode: "local"
|
||||||
location_type: "local"
|
location_type: "local"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Critical services that must be running for provisioning to work
|
# Critical services that must be running for provisioning to work.
|
||||||
def get-critical-services [] {
|
# Only the orchestrator is required for L2+ deployments; control-center
|
||||||
# Get service endpoints from config
|
# and kms-service are optional platform features.
|
||||||
|
def get-critical-services []: nothing -> list<record> {
|
||||||
let orchestrator_endpoint = (
|
let orchestrator_endpoint = (
|
||||||
config-get "platform.orchestrator.endpoint" "http://localhost:9090/health"
|
config-get "platform.orchestrator.endpoint" "http://localhost:9011/health"
|
||||||
)
|
|
||||||
|
|
||||||
let control_center_url = (
|
|
||||||
config-get "platform.control_center.url" "http://localhost:3000"
|
|
||||||
)
|
|
||||||
let control_center_endpoint = $control_center_url + "/health"
|
|
||||||
|
|
||||||
let kms_endpoint = (
|
|
||||||
config-get "platform.kms.endpoint" "http://localhost:3001/health"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
[
|
[
|
||||||
|
|
@ -75,20 +91,6 @@ def get-critical-services [] {
|
||||||
timeout: 30
|
timeout: 30
|
||||||
description: "Workflow orchestrator"
|
description: "Workflow orchestrator"
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name: "control-center"
|
|
||||||
health_check: "http"
|
|
||||||
endpoint: $control_center_endpoint
|
|
||||||
timeout: 30
|
|
||||||
description: "Control center and authentication"
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name: "kms-service"
|
|
||||||
health_check: "http"
|
|
||||||
endpoint: $kms_endpoint
|
|
||||||
timeout: 30
|
|
||||||
description: "KMS service (RustyVault)"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,6 +113,86 @@ def check-service-health [service: record] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Helper to process a single service for bootstrap
|
||||||
|
def process-service-bootstrap [
|
||||||
|
service: record
|
||||||
|
auto_start: bool
|
||||||
|
verbose: bool
|
||||||
|
timeout: int
|
||||||
|
] {
|
||||||
|
if $verbose {
|
||||||
|
print $"📋 Checking ($service.name)..."
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_healthy = (check-service-health $service)
|
||||||
|
|
||||||
|
if $is_healthy {
|
||||||
|
if $verbose {
|
||||||
|
print $" ✅ ($service.name) is healthy"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: $service.name
|
||||||
|
status: "healthy"
|
||||||
|
action: "none"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if $verbose {
|
||||||
|
print $" ⚠️ ($service.name) is not responding"
|
||||||
|
}
|
||||||
|
|
||||||
|
if $auto_start {
|
||||||
|
if $verbose {
|
||||||
|
print $" 🚀 Starting ($service.name)..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to start the service
|
||||||
|
let start_result = (
|
||||||
|
not ((start-platform-service $service.name --verbose=$verbose) == null)
|
||||||
|
)
|
||||||
|
|
||||||
|
if $start_result {
|
||||||
|
# Wait for service to be healthy
|
||||||
|
let wait_result = (wait-for-service-health $service --timeout=$timeout --verbose=$verbose)
|
||||||
|
|
||||||
|
if $wait_result {
|
||||||
|
if $verbose {
|
||||||
|
print $" ✅ ($service.name) started successfully"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: $service.name
|
||||||
|
status: "healthy"
|
||||||
|
action: "started"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if $verbose {
|
||||||
|
print $" ❌ ($service.name) failed to become healthy"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: $service.name
|
||||||
|
status: "unhealthy"
|
||||||
|
action: "failed_to_start"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if $verbose {
|
||||||
|
print $" ❌ Failed to start ($service.name)"
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name: $service.name
|
||||||
|
status: "unhealthy"
|
||||||
|
action: "start_failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
name: $service.name
|
||||||
|
status: "unhealthy"
|
||||||
|
action: "not_running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Bootstrap platform services
|
# Bootstrap platform services
|
||||||
export def bootstrap-platform [
|
export def bootstrap-platform [
|
||||||
--auto-start (-a) # Automatically start services if not running
|
--auto-start (-a) # Automatically start services if not running
|
||||||
|
|
@ -120,8 +202,6 @@ export def bootstrap-platform [
|
||||||
] {
|
] {
|
||||||
|
|
||||||
let critical_services = (get-critical-services)
|
let critical_services = (get-critical-services)
|
||||||
mut services_status = []
|
|
||||||
mut all_healthy = true
|
|
||||||
|
|
||||||
if $verbose {
|
if $verbose {
|
||||||
print $"🔧 Bootstrapping platform services..."
|
print $"🔧 Bootstrapping platform services..."
|
||||||
|
|
@ -129,82 +209,13 @@ export def bootstrap-platform [
|
||||||
print ""
|
print ""
|
||||||
}
|
}
|
||||||
|
|
||||||
for service in $critical_services {
|
# Process each service using helper function to avoid closure variable capture
|
||||||
if $verbose {
|
let services_status = ($critical_services | each { |service|
|
||||||
print $"📋 Checking ($service.name)..."
|
process-service-bootstrap $service $auto_start $verbose $timeout
|
||||||
}
|
})
|
||||||
|
|
||||||
let is_healthy = (check-service-health $service)
|
# Check if all services are healthy
|
||||||
|
let all_healthy = ($services_status | all { |s| $s.status == "healthy" })
|
||||||
if $is_healthy {
|
|
||||||
if $verbose {
|
|
||||||
print $" ✅ ($service.name) is healthy"
|
|
||||||
}
|
|
||||||
$services_status = ($services_status | append {
|
|
||||||
name: $service.name
|
|
||||||
status: "healthy"
|
|
||||||
action: "none"
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if $verbose {
|
|
||||||
print $" ⚠️ ($service.name) is not responding"
|
|
||||||
}
|
|
||||||
|
|
||||||
if $auto_start {
|
|
||||||
if $verbose {
|
|
||||||
print $" 🚀 Starting ($service.name)..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Try to start the service
|
|
||||||
let start_result = (
|
|
||||||
not ((start-platform-service $service.name --verbose=$verbose) == null)
|
|
||||||
)
|
|
||||||
|
|
||||||
if $start_result {
|
|
||||||
# Wait for service to be healthy
|
|
||||||
let wait_result = (wait-for-service-health $service --timeout=$timeout --verbose=$verbose)
|
|
||||||
|
|
||||||
if $wait_result {
|
|
||||||
if $verbose {
|
|
||||||
print $" ✅ ($service.name) started successfully"
|
|
||||||
}
|
|
||||||
$services_status = ($services_status | append {
|
|
||||||
name: $service.name
|
|
||||||
status: "healthy"
|
|
||||||
action: "started"
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if $verbose {
|
|
||||||
print $" ❌ ($service.name) failed to become healthy"
|
|
||||||
}
|
|
||||||
$services_status = ($services_status | append {
|
|
||||||
name: $service.name
|
|
||||||
status: "unhealthy"
|
|
||||||
action: "failed_to_start"
|
|
||||||
})
|
|
||||||
$all_healthy = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if $verbose {
|
|
||||||
print $" ❌ Failed to start ($service.name)"
|
|
||||||
}
|
|
||||||
$services_status = ($services_status | append {
|
|
||||||
name: $service.name
|
|
||||||
status: "unhealthy"
|
|
||||||
action: "start_failed"
|
|
||||||
})
|
|
||||||
$all_healthy = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$services_status = ($services_status | append {
|
|
||||||
name: $service.name
|
|
||||||
status: "unhealthy"
|
|
||||||
action: "not_running"
|
|
||||||
})
|
|
||||||
$all_healthy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if $verbose {
|
if $verbose {
|
||||||
print ""
|
print ""
|
||||||
|
|
@ -233,11 +244,12 @@ def start-platform-service [
|
||||||
|
|
||||||
if $verbose {
|
if $verbose {
|
||||||
print $" Deployment mode: ($deployment_mode)"
|
print $" Deployment mode: ($deployment_mode)"
|
||||||
print $" Deployment location: ($deployment_location.type)"
|
print $" Deployment location: ($deployment_location.location_type)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Route to appropriate startup method based on deployment mode
|
# Route to appropriate startup method based on deployment mode
|
||||||
match $deployment_mode {
|
match $deployment_mode {
|
||||||
|
"local" => { start-service-local $service_name --verbose=$verbose }
|
||||||
"docker-compose" => { start-service-docker-compose $service_name --verbose=$verbose }
|
"docker-compose" => { start-service-docker-compose $service_name --verbose=$verbose }
|
||||||
"kubernetes" => { start-service-kubernetes $service_name --verbose=$verbose }
|
"kubernetes" => { start-service-kubernetes $service_name --verbose=$verbose }
|
||||||
"remote-ssh" => { start-service-remote-ssh $service_name --verbose=$verbose }
|
"remote-ssh" => { start-service-remote-ssh $service_name --verbose=$verbose }
|
||||||
|
|
@ -256,7 +268,7 @@ def start-service-docker-compose [
|
||||||
service_name: string
|
service_name: string
|
||||||
--verbose (-v)
|
--verbose (-v)
|
||||||
] {
|
] {
|
||||||
let platform_path = (config-get "platform.docker_compose.path" (get-base-path | path join "platform"))
|
let platform_path = (config-get "platform.docker_compose.path" (get-config-base-path | path join "platform"))
|
||||||
let compose_file = ($platform_path | path join "docker-compose.yaml")
|
let compose_file = ($platform_path | path join "docker-compose.yaml")
|
||||||
|
|
||||||
if not ($compose_file | path exists) {
|
if not ($compose_file | path exists) {
|
||||||
|
|
@ -284,6 +296,123 @@ def start-service-docker-compose [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Start service locally via native binary or systemd
|
||||||
|
def start-service-local [
|
||||||
|
service_name: string
|
||||||
|
--verbose (-v)
|
||||||
|
] {
|
||||||
|
let os_type = $nu.os-info.name
|
||||||
|
|
||||||
|
# On Linux, try systemd first
|
||||||
|
if $os_type == "linux" {
|
||||||
|
let systemd_result = (do {
|
||||||
|
if $verbose {
|
||||||
|
print $" Trying systemd: systemctl start ($service_name)"
|
||||||
|
}
|
||||||
|
systemctl start $service_name
|
||||||
|
} | complete)
|
||||||
|
|
||||||
|
if $systemd_result.exit_code == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback (all OS): try binary in ~/.local/bin/ with provisioning- prefix
|
||||||
|
let bin_dir = ($env.HOME | path join ".local" "bin")
|
||||||
|
let local_bin = ($bin_dir | path join $"provisioning-($service_name)")
|
||||||
|
let config_base = (get-config-base-path)
|
||||||
|
let config_dir = ($config_base | path join "platform" "config")
|
||||||
|
|
||||||
|
if ($local_bin | path exists) {
|
||||||
|
if $verbose {
|
||||||
|
print $" Running binary: ($local_bin)"
|
||||||
|
print $" Config dir: ($config_dir)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derive NICKEL_IMPORT_PATH from config base path automatically
|
||||||
|
# Two cases:
|
||||||
|
# 1. Development: /path/to/project/provisioning/../platform/config/
|
||||||
|
# → Look for provisioning/ at project root level
|
||||||
|
# 2. User install: ~/.config/provisioning/platform/config/ (Linux) or
|
||||||
|
# ~/Library/Application Support/provisioning/platform/config/ (macOS)
|
||||||
|
# → Use PROVISIONING env var pointing to the development project
|
||||||
|
let nickel_import_path = (do {
|
||||||
|
let normalized_config = ($config_base | path expand)
|
||||||
|
# Go up 2 directories: config -> platform -> project_root
|
||||||
|
let project_root = ($normalized_config | path dirname | path dirname)
|
||||||
|
let provisioning_dir = ($project_root | path join "provisioning")
|
||||||
|
|
||||||
|
# Case 1: Check if provisioning/ exists at project root level (local development)
|
||||||
|
if ($provisioning_dir | path exists) {
|
||||||
|
if $verbose {
|
||||||
|
print $" NICKEL_IMPORT_PATH (local): ($provisioning_dir)"
|
||||||
|
}
|
||||||
|
$provisioning_dir
|
||||||
|
} else {
|
||||||
|
# Case 2: User install - check if in standard user config location by OS
|
||||||
|
let config_str = ($normalized_config | into string)
|
||||||
|
let os_type = $nu.os-info.name
|
||||||
|
|
||||||
|
# Determine standard user config path for this OS
|
||||||
|
let user_config_path = if $os_type == "linux" {
|
||||||
|
($env.HOME | path join ".config" "provisioning")
|
||||||
|
} else if $os_type == "macos" {
|
||||||
|
($env.HOME | path join "Library" "Application Support" "provisioning")
|
||||||
|
} else {
|
||||||
|
# Windows or other: try both paths
|
||||||
|
($env.HOME | path join ".config" "provisioning")
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_user_config = ($config_str | str starts-with ($user_config_path | path expand))
|
||||||
|
|
||||||
|
if $is_user_config {
|
||||||
|
# For user installs, rely on PROVISIONING env var pointing to the development project
|
||||||
|
if $verbose {
|
||||||
|
print $" User config location detected ($os_type): ($config_str)"
|
||||||
|
print $" Using PROVISIONING env var for schemas"
|
||||||
|
}
|
||||||
|
$env.PROVISIONING? | default "/provisioning"
|
||||||
|
} else {
|
||||||
|
# Fallback for other cases
|
||||||
|
if $verbose {
|
||||||
|
print $" ⚠️ Could not determine provisioning location"
|
||||||
|
}
|
||||||
|
$env.PROVISIONING? | default "/provisioning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let result = (do {
|
||||||
|
if $verbose {
|
||||||
|
# Show output during verbose mode for debugging
|
||||||
|
with-env { NICKEL_IMPORT_PATH: $nickel_import_path } {
|
||||||
|
^sh -c $"'($local_bin)' --config-dir '($config_dir)' &"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with-env { NICKEL_IMPORT_PATH: $nickel_import_path } {
|
||||||
|
^sh -c $"'($local_bin)' --config-dir '($config_dir)' > /dev/null 2>&1 &"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} | complete)
|
||||||
|
|
||||||
|
if $result.exit_code == 0 {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
if $verbose {
|
||||||
|
print $" Error starting binary: ($result.stderr)"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if $verbose {
|
||||||
|
print $" ❌ Could not start ($service_name)"
|
||||||
|
print $" - systemd not available on ($os_type)"
|
||||||
|
print $" - binary not found: ($local_bin)"
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
# Start service via Kubernetes
|
# Start service via Kubernetes
|
||||||
def start-service-kubernetes [
|
def start-service-kubernetes [
|
||||||
service_name: string
|
service_name: string
|
||||||
|
|
@ -291,7 +420,7 @@ def start-service-kubernetes [
|
||||||
] {
|
] {
|
||||||
let kubeconfig = (config-get "platform.kubernetes.kubeconfig" "")
|
let kubeconfig = (config-get "platform.kubernetes.kubeconfig" "")
|
||||||
let namespace = (config-get "platform.kubernetes.namespace" "default")
|
let namespace = (config-get "platform.kubernetes.namespace" "default")
|
||||||
let manifests_path = (config-get "platform.kubernetes.manifests_path" (get-base-path | path join "platform" "k8s"))
|
let manifests_path = (config-get "platform.kubernetes.manifests_path" (get-config-base-path | path join "platform" "k8s"))
|
||||||
|
|
||||||
if $verbose {
|
if $verbose {
|
||||||
print $" Kubernetes namespace: ($namespace)"
|
print $" Kubernetes namespace: ($namespace)"
|
||||||
|
|
|
||||||
|
|
@ -127,15 +127,18 @@ export def platform-health [] {
|
||||||
# Start platform services
|
# Start platform services
|
||||||
export def platform-start [] {
|
export def platform-start [] {
|
||||||
print ""
|
print ""
|
||||||
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
print "Starting Platform Services"
|
print "Starting Platform Services"
|
||||||
print "=========================="
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
print ""
|
print ""
|
||||||
|
|
||||||
start-required-services
|
start-required-services
|
||||||
|
|
||||||
print ""
|
print ""
|
||||||
print "Waiting for services to be ready..."
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
sleep 2sec
|
print "Platform Health Status"
|
||||||
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
print ""
|
||||||
|
|
||||||
platform-health
|
platform-health
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,77 @@
|
||||||
# Platform Service Health Checks
|
# Platform Service Health Checks
|
||||||
# Provides health checking functionality for platform services
|
|
||||||
|
|
||||||
use target.nu *
|
use target.nu *
|
||||||
|
|
||||||
# Check if a service is healthy at its endpoint
|
# Check if service is healthy at its port
|
||||||
export def check-service-health [service: string] {
|
export def check-service-health [service: string] {
|
||||||
let config = (get-platform-service-config $service)
|
let config = (get-deployment-service-config $service)
|
||||||
let endpoint = $config.endpoint
|
let enabled = ($config.enabled? | default false)
|
||||||
let health_path = ($config.health_check.endpoint | default "/health")
|
|
||||||
let timeout = ($config.health_check.timeout_ms | default 5000)
|
|
||||||
|
|
||||||
let health_url = $"($endpoint)($health_path)"
|
if not $enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
# Try to reach the health endpoint - services are likely not running
|
# Extract port
|
||||||
# Just return false since they're not started yet
|
let port = (
|
||||||
false
|
if (($config.server?) != null) {
|
||||||
|
$config.server.port
|
||||||
|
} else if (($config.port?) != null) {
|
||||||
|
$config.port
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check using platform-specific command — always filter by port to avoid full scan
|
||||||
|
if ($nu.os-info.name == "macos") {
|
||||||
|
let result = (do { ^lsof -i $":($port)" -P -n } | complete)
|
||||||
|
($result.exit_code == 0) and ($result.stdout | str contains "LISTEN")
|
||||||
|
} else {
|
||||||
|
let result = (do { ^ss -tlnp $"sport = :($port)" } | complete)
|
||||||
|
if ($result.exit_code == 0) {
|
||||||
|
($result.stdout | lines | skip 1 | length) > 0
|
||||||
|
} else {
|
||||||
|
# fallback: netstat with port grep
|
||||||
|
let r2 = (do { ^netstat -tlnp } | complete)
|
||||||
|
($r2.exit_code == 0) and ($r2.stdout | str contains $":($port) ")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check all enabled services
|
# Check all enabled services
|
||||||
export def check-all-services [] {
|
export def check-all-services [] {
|
||||||
let services = (list-services)
|
let services = (get-enabled-services)
|
||||||
|
|
||||||
mut result = []
|
$services | each {|item|
|
||||||
for svc in $services {
|
|
||||||
let healthy = (check-service-health $svc)
|
|
||||||
$result = ($result | append {
|
|
||||||
name: $svc
|
|
||||||
status: (if $healthy { "healthy" } else { "unhealthy" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$result
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get health status for all required services
|
|
||||||
export def check-required-services [] {
|
|
||||||
let required = (list-required-services)
|
|
||||||
|
|
||||||
mut result = []
|
|
||||||
for item in $required {
|
|
||||||
let healthy = (check-service-health $item.name)
|
let healthy = (check-service-health $item.name)
|
||||||
$result = ($result | append {
|
{
|
||||||
name: $item.name
|
name: $item.name
|
||||||
status: (if $healthy { "healthy" } else { "unhealthy" })
|
status: (if $healthy { "healthy" } else { "unhealthy" })
|
||||||
required: true
|
priority: $item.priority
|
||||||
})
|
|
||||||
}
|
|
||||||
$result
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wait for a service to become healthy
|
|
||||||
export def wait-for-service [service: string, --timeout_seconds: int = 30] {
|
|
||||||
let start = (date now)
|
|
||||||
let timeout = ($timeout_seconds * 1000)
|
|
||||||
|
|
||||||
mut healthy = false
|
|
||||||
mut attempts = 0
|
|
||||||
|
|
||||||
while (not $healthy) and ($attempts < 60) {
|
|
||||||
if (check-service-health $service) {
|
|
||||||
$healthy = true
|
|
||||||
} else {
|
|
||||||
sleep 500ms
|
|
||||||
$attempts = ($attempts + 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
$healthy
|
|
||||||
|
# Get health status for all services
|
||||||
|
export def check-required-services [] {
|
||||||
|
check-all-services
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for service to become healthy
|
||||||
|
export def wait-for-service [service: string, --timeout_seconds: int = 30] {
|
||||||
|
let max_attempts = 60
|
||||||
|
let attempt_list = (seq 1 $max_attempts)
|
||||||
|
|
||||||
|
let results = (
|
||||||
|
$attempt_list | each {|_attempt|
|
||||||
|
if (check-service-health $service) {
|
||||||
|
"healthy"
|
||||||
|
} else {
|
||||||
|
sleep 500ms
|
||||||
|
"checking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
($results | any {|status| $status == "healthy"})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@
|
||||||
# - Auto-start service management
|
# - Auto-start service management
|
||||||
# - Credential and token management
|
# - Credential and token management
|
||||||
# - Connection metadata tracking
|
# - Connection metadata tracking
|
||||||
|
# - Service startup management and lifecycle
|
||||||
# - CLI commands
|
# - CLI commands
|
||||||
|
|
||||||
export use activation.nu *
|
|
||||||
export use target.nu *
|
export use target.nu *
|
||||||
export use discovery.nu *
|
export use discovery.nu *
|
||||||
export use health.nu *
|
export use health.nu *
|
||||||
export use autostart.nu *
|
|
||||||
export use credentials.nu *
|
export use credentials.nu *
|
||||||
export use connection.nu *
|
export use connection.nu *
|
||||||
export use cli.nu *
|
export use cli.nu *
|
||||||
export use provctl.nu *
|
export use autostart.nu *
|
||||||
|
export use service-manager.nu *
|
||||||
|
|
|
||||||
573
nulib/lib_provisioning/platform/service-manager.nu
Normal file
573
nulib/lib_provisioning/platform/service-manager.nu
Normal file
|
|
@ -0,0 +1,573 @@
|
||||||
|
# Platform Service Manager - Service management for deployment
|
||||||
|
# Handles loading deployment configuration and service management
|
||||||
|
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval, ncl-eval-soft]
|
||||||
|
use ../user/config.nu [get-active-workspace-details]
|
||||||
|
|
||||||
|
# Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
|
||||||
|
# Returns the normalized name (e.g., "provisioning_daemon" → "daemon")
|
||||||
|
export def normalize-service-name [service_name: string] {
|
||||||
|
if ($service_name | str starts-with "provisioning-") {
|
||||||
|
$service_name | str replace "provisioning-" ""
|
||||||
|
} else if ($service_name | str starts-with "provisioning_") {
|
||||||
|
$service_name | str replace "provisioning_" ""
|
||||||
|
} else {
|
||||||
|
$service_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load deployment mode configuration from Nickel
|
||||||
|
export def load-deployment-mode [] {
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
|
||||||
|
# Try multiple possible locations for the deployment-mode.ncl file
|
||||||
|
let possible_paths = [
|
||||||
|
# First try the explicit env var
|
||||||
|
($env.PROVISIONING_USER_PLATFORM? | default null),
|
||||||
|
# macOS: Library/Application Support
|
||||||
|
($home | path join "Library/Application Support/provisioning/platform"),
|
||||||
|
# Linux/other: .config
|
||||||
|
($home | path join ".config/provisioning/platform"),
|
||||||
|
]
|
||||||
|
|
||||||
|
let config_file = (
|
||||||
|
$possible_paths
|
||||||
|
| where { |p| $p != null }
|
||||||
|
| each { |p| $p | path expand | path join "deployment-mode.ncl" }
|
||||||
|
| where { |p| $p | path exists }
|
||||||
|
| get 0?
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($config_file == null) {
|
||||||
|
error make {msg: "Deployment mode file not found in any of the expected locations"}
|
||||||
|
}
|
||||||
|
|
||||||
|
let import_path = ($env.PROVISIONING? | default "")
|
||||||
|
let import_paths = if ($import_path | is-not-empty) { [$import_path] } else { [] }
|
||||||
|
ncl-eval $config_file $import_paths
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load individual service configuration
|
||||||
|
export def load-service-config [service_name: string] {
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
|
||||||
|
# Try multiple possible base paths
|
||||||
|
let possible_bases = [
|
||||||
|
# First try the explicit env var
|
||||||
|
($env.PROVISIONING_USER_PLATFORM? | default null),
|
||||||
|
# macOS: Library/Application Support
|
||||||
|
($home | path join "Library/Application Support/provisioning/platform"),
|
||||||
|
# Linux/other: .config
|
||||||
|
($home | path join ".config/provisioning/platform"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Try to find the config file
|
||||||
|
let config_file = (
|
||||||
|
$possible_bases
|
||||||
|
| where { |b| $b != null }
|
||||||
|
| each { |base|
|
||||||
|
# Try both the underscore and dash versions
|
||||||
|
let base_expanded = ($base | path expand)
|
||||||
|
let path1 = ($base_expanded | path join "config" ($"($service_name).ncl"))
|
||||||
|
let path2 = ($base_expanded | path join "config" ($"($service_name | str replace "_" "-").ncl"))
|
||||||
|
|
||||||
|
if ($path1 | path exists) {
|
||||||
|
$path1
|
||||||
|
} else if ($path2 | path exists) {
|
||||||
|
$path2
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| where { |p| $p != null }
|
||||||
|
| get 0?
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($config_file == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
ncl-eval-soft $config_file [] null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the port for a service from its configuration
|
||||||
|
export def get-service-port [service_name: string] {
|
||||||
|
let config = (load-service-config $service_name)
|
||||||
|
|
||||||
|
if ($config == null) {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to extract port from the service configuration
|
||||||
|
# Different services store the port in different locations
|
||||||
|
let service_key = $service_name | str replace "-" "_"
|
||||||
|
|
||||||
|
if ($config | get --optional $service_key) != null {
|
||||||
|
let service_config = ($config | get $service_key)
|
||||||
|
|
||||||
|
# Try common port locations in order
|
||||||
|
# 1. server.port (most services: orchestrator, vault_service, etc.)
|
||||||
|
if ($service_config | get --optional "server") != null {
|
||||||
|
if ($service_config.server | get --optional "port") != null {
|
||||||
|
return ($service_config.server.port | into string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. build.port (RAG service)
|
||||||
|
if ($service_config | get --optional "build") != null {
|
||||||
|
if ($service_config.build | get --optional "port") != null {
|
||||||
|
return ($service_config.build.port | into string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. http.port (some services)
|
||||||
|
if ($service_config | get --optional "http") != null {
|
||||||
|
if ($service_config.http | get --optional "port") != null {
|
||||||
|
return ($service_config.http.port | into string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. port at root level
|
||||||
|
if ($service_config | get --optional "port") != null {
|
||||||
|
return ($service_config.port | into string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"?"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start required services based on deployment configuration
|
||||||
|
export def start-required-services [] {
|
||||||
|
let deployment = (load-deployment-mode)
|
||||||
|
|
||||||
|
# Get enabled services from deployment config
|
||||||
|
let services = $deployment.services
|
||||||
|
let all_service_names = ($services | columns)
|
||||||
|
|
||||||
|
# Filter to enabled services
|
||||||
|
let enabled_services = (
|
||||||
|
$all_service_names
|
||||||
|
| where {|name|
|
||||||
|
let config = ($services | get $name)
|
||||||
|
($config | get --optional "enabled" | default false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($enabled_services | length) == 0 {
|
||||||
|
print "⚠ No services enabled in deployment-mode.ncl"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current running processes once
|
||||||
|
let running_processes = (^ps aux)
|
||||||
|
|
||||||
|
# Start each enabled service
|
||||||
|
for service_name in $enabled_services {
|
||||||
|
let port = (get-service-port $service_name)
|
||||||
|
let normalized_name = (normalize-service-name $service_name)
|
||||||
|
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
|
||||||
|
let is_running = ($running_processes | str contains $binary_name)
|
||||||
|
|
||||||
|
# Check if binary exists
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
let binary_path = ($home | path join ".local/bin" $binary_name)
|
||||||
|
|
||||||
|
if not ($binary_path | path exists) {
|
||||||
|
print $"✗ ($service_name) binary not found"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# If already running, just report it
|
||||||
|
if $is_running {
|
||||||
|
let status_msg = $"((ansi green))started((ansi reset))"
|
||||||
|
print $"✓ ($service_name) on port ($port) — ($status_msg)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start in background
|
||||||
|
print $"→ Starting ($service_name)..."
|
||||||
|
|
||||||
|
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 = ($home | path join "Library/Application Support/provisioning/platform")
|
||||||
|
let provisioning_path = ($home | path join ".local/bin/provisioning")
|
||||||
|
|
||||||
|
# Build start command (only orchestrator accepts --provisioning-path)
|
||||||
|
let start_cmd = if ($normalized_name == "orchestrator") {
|
||||||
|
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
|
||||||
|
} else {
|
||||||
|
$"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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ncl-sync: Nickel config cache daemon — always started, independent of deployment mode.
|
||||||
|
ncl-sync-start
|
||||||
|
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start specific services by name
|
||||||
|
# Usage: start-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
|
||||||
|
export def start-services [service_list: list<string>] {
|
||||||
|
# Parse service names (handle comma-separated strings)
|
||||||
|
let services = (
|
||||||
|
$service_list
|
||||||
|
| each { |item| $item | split row "," | each { |s| $s | str trim } }
|
||||||
|
| flatten
|
||||||
|
| where { |s| ($s | is-not-empty) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($services | length) == 0 {
|
||||||
|
print "⚠ No services specified"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current running processes once
|
||||||
|
let running_processes = (^ps aux)
|
||||||
|
|
||||||
|
# Start each service
|
||||||
|
for service_name in $services {
|
||||||
|
let normalized_name = (normalize-service-name $service_name)
|
||||||
|
let port = (get-service-port $normalized_name)
|
||||||
|
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
|
||||||
|
let is_running = ($running_processes | str contains $binary_name)
|
||||||
|
|
||||||
|
# Check if binary exists
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
let binary_path = ($home | path join ".local/bin" $binary_name)
|
||||||
|
|
||||||
|
if not ($binary_path | path exists) {
|
||||||
|
print $"✗ ($service_name) binary not found"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# If already running, just report it
|
||||||
|
if $is_running {
|
||||||
|
let status_msg = $"((ansi green))started((ansi reset))"
|
||||||
|
print $"✓ ($service_name) on port ($port) — ($status_msg)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start in background
|
||||||
|
print $"→ Starting ($service_name)..."
|
||||||
|
|
||||||
|
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 = ($home | path join "Library/Application Support/provisioning/platform")
|
||||||
|
let provisioning_path = ($home | path join ".local/bin/provisioning")
|
||||||
|
|
||||||
|
# Build start command (only orchestrator accepts --provisioning-path)
|
||||||
|
let start_cmd = if ($normalized_name == "orchestrator") {
|
||||||
|
$"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &"
|
||||||
|
} else {
|
||||||
|
$"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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop specific services by name
|
||||||
|
# Usage: stop-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"]
|
||||||
|
export def stop-services [service_list: list<string>] {
|
||||||
|
# Parse service names (handle comma-separated strings)
|
||||||
|
let services = (
|
||||||
|
$service_list
|
||||||
|
| each { |item| $item | split row "," | each { |s| $s | str trim } }
|
||||||
|
| flatten
|
||||||
|
| where { |s| ($s | is-not-empty) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($services | length) == 0 {
|
||||||
|
print "⚠ No services specified"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get current running processes once
|
||||||
|
let running_processes = (^ps aux)
|
||||||
|
|
||||||
|
# Stop each service
|
||||||
|
for service_name in $services {
|
||||||
|
# Normalize service name: strip "provisioning-" or "provisioning_" prefix if present
|
||||||
|
let normalized_name = (
|
||||||
|
if ($service_name | str starts-with "provisioning-") {
|
||||||
|
$service_name | str replace "provisioning-" ""
|
||||||
|
} else if ($service_name | str starts-with "provisioning_") {
|
||||||
|
$service_name | str replace "provisioning_" ""
|
||||||
|
} else {
|
||||||
|
$service_name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let port = (get-service-port $normalized_name)
|
||||||
|
let binary_name = $"provisioning-($normalized_name | str replace "_" "-")"
|
||||||
|
let is_running = ($running_processes | str contains $binary_name)
|
||||||
|
|
||||||
|
if $is_running {
|
||||||
|
(^sh -c $"pkill -f '($binary_name)' 2>/dev/null || true") | ignore
|
||||||
|
sleep 500ms
|
||||||
|
let stopped_msg = $"((ansi red))stopped((ansi reset))"
|
||||||
|
print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
|
||||||
|
} else {
|
||||||
|
let stopped_msg = $"((ansi red))already stopped((ansi reset))"
|
||||||
|
print $"✓ ($service_name) on port ($port) — ($stopped_msg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a port is listening (health check for external services)
|
||||||
|
export def is-port-listening [port: number] {
|
||||||
|
let uname_result = (do { ^uname -s } | complete)
|
||||||
|
let os_type = (if $uname_result.exit_code == 0 { $uname_result.stdout | str trim } else { "Linux" })
|
||||||
|
|
||||||
|
if $os_type == "Darwin" {
|
||||||
|
# macOS: use lsof to check listening ports
|
||||||
|
# Pattern matches both "*:PORT" and "127.0.0.1:PORT" formats
|
||||||
|
let check = (do { ^lsof -i -P -n } | complete)
|
||||||
|
if $check.exit_code == 0 {
|
||||||
|
let port_str = $"($port)"
|
||||||
|
$check.stdout | str contains $"($port_str)" | if $in { true } else { false }
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Linux: use netstat to check listening ports
|
||||||
|
let check = (do { ^netstat -tuln } | complete)
|
||||||
|
if $check.exit_code == 0 {
|
||||||
|
$check.stdout | str contains $":$port"
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get external services from user configuration file
|
||||||
|
export def get-external-services [] {
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
|
||||||
|
# Try multiple possible base paths for external services config
|
||||||
|
let possible_bases = [
|
||||||
|
# First try the explicit env var
|
||||||
|
($env.PROVISIONING_USER_PLATFORM? | default null),
|
||||||
|
# macOS: Library/Application Support
|
||||||
|
($home | path join "Library/Application Support/provisioning/platform"),
|
||||||
|
# Linux/other: .config
|
||||||
|
($home | path join ".config/provisioning/platform"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Try to find the external services config file
|
||||||
|
let external_services_file = (
|
||||||
|
$possible_bases
|
||||||
|
| where { |b| $b != null }
|
||||||
|
| each { |base| ($base | path expand | path join "config/external-services.ncl") }
|
||||||
|
| where { |p| $p | path exists }
|
||||||
|
| get 0?
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($external_services_file == null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
ncl-eval-soft $external_services_file [] [] | default []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start nats-server as a child process for solo mode.
|
||||||
|
# Requires nats-server to be in PATH.
|
||||||
|
# Returns a record with {pid, port, jetstream_dir} on success.
|
||||||
|
export def nats_start [config: record]: nothing -> record {
|
||||||
|
let port = ($config.port? | default 4222)
|
||||||
|
let data_dir = ($env.HOME | path join ".local/share/provisioning/nats")
|
||||||
|
let js_dir = ($config.jetstream_store_dir? | default $data_dir)
|
||||||
|
|
||||||
|
let mk_result = (do { ^mkdir -p $js_dir } | complete)
|
||||||
|
if ($mk_result.exit_code != 0) {
|
||||||
|
error make {msg: $"Failed to create NATS data dir ($js_dir): ($mk_result.stderr)"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Spawn nats-server in background — nohup keeps it alive after this shell exits
|
||||||
|
let cmd = $"nohup nats-server -js -sd ($js_dir) -p ($port) >/dev/null 2>&1 &"
|
||||||
|
let start_result = (do { ^sh -c $cmd } | complete)
|
||||||
|
if ($start_result.exit_code != 0) {
|
||||||
|
error make {msg: $"nats-server failed to start: ($start_result.stderr)"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Poll for readiness — up to 10 seconds in 500ms increments
|
||||||
|
let ready = (
|
||||||
|
1..20
|
||||||
|
| each { |_i|
|
||||||
|
sleep 500ms
|
||||||
|
(nats_health {port: $port})
|
||||||
|
}
|
||||||
|
| where { |r| $r }
|
||||||
|
| length
|
||||||
|
| $in > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (not $ready) {
|
||||||
|
error make {msg: "nats-server did not become ready within 10 seconds"}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pid_result = (do { ^pgrep -f $"nats-server" } | complete)
|
||||||
|
let pid = (if ($pid_result.exit_code == 0) { $pid_result.stdout | lines | get 0? | default "0" | into int } else { 0 })
|
||||||
|
|
||||||
|
{pid: $pid, port: $port, jetstream_dir: $js_dir}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop the nats-server process.
|
||||||
|
export def nats_stop [config: record]: nothing -> nothing {
|
||||||
|
let port = ($config.port? | default 4222)
|
||||||
|
let kill_result = (do { ^pkill -f "nats-server" } | complete)
|
||||||
|
if ($kill_result.exit_code != 0) {
|
||||||
|
print $"Warning: nats-server on port ($port) was not running or could not be stopped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if nats-server is accepting TCP connections on the configured port.
|
||||||
|
# Returns true if healthy, false otherwise.
|
||||||
|
export def nats_health [config: record]: nothing -> bool {
|
||||||
|
let port = ($config.port? | default 4222)
|
||||||
|
let check = (do { ^nc -z -w 1 127.0.0.1 $port } | complete)
|
||||||
|
$check.exit_code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ncl-sync daemon management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def ncl-sync-cache-dir []: nothing -> string {
|
||||||
|
if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
|
||||||
|
# Walk up from PWD to find workspace root
|
||||||
|
let pwd = $env.PWD
|
||||||
|
let ws_pwd = if ($pwd | path join "infra" | path exists) or ($pwd | path join "config" "provisioning.ncl" | path exists) or ($pwd | path join ".ontology" | path exists) {
|
||||||
|
$pwd
|
||||||
|
} else { "" }
|
||||||
|
if ($ws_pwd | is-not-empty) { return ($ws_pwd | path join ".ncl-cache") }
|
||||||
|
# Fallback to active workspace from user_config
|
||||||
|
let details = (get-active-workspace-details)
|
||||||
|
if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
|
||||||
|
return ($details.path | path join ".ncl-cache")
|
||||||
|
}
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
$home | path join ".cache" "provisioning" "config-cache"
|
||||||
|
}
|
||||||
|
|
||||||
|
def ncl-sync-pid-file []: nothing -> string {
|
||||||
|
(ncl-sync-cache-dir) | path join "ncl-sync.pid"
|
||||||
|
}
|
||||||
|
|
||||||
|
def ncl-sync-bin []: nothing -> string {
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
$home | path join ".local" "bin" "provisioning-ncl-sync"
|
||||||
|
}
|
||||||
|
|
||||||
|
def ncl-sync-running []: nothing -> bool {
|
||||||
|
let pid_file = (ncl-sync-pid-file)
|
||||||
|
if not ($pid_file | path exists) { return false }
|
||||||
|
let pid = (open $pid_file | str trim)
|
||||||
|
if ($pid | is-empty) { return false }
|
||||||
|
let check = (do { ^kill -0 ($pid | into int) } | complete)
|
||||||
|
$check.exit_code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that a path has workspace markers (infra/, config/provisioning.ncl, or .ontology/).
|
||||||
|
def is-workspace-dir [path: string]: nothing -> bool {
|
||||||
|
if ($path | is-empty) or (not ($path | path exists)) { return false }
|
||||||
|
let has_infra = ($path | path join "infra" | path exists)
|
||||||
|
let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
|
||||||
|
let has_onto = ($path | path join ".ontology" | path exists)
|
||||||
|
$has_infra or $has_config or $has_onto
|
||||||
|
}
|
||||||
|
|
||||||
|
# Walk up from `path` until a workspace root is found or we reach filesystem root.
|
||||||
|
def find-workspace-up [path: string]: nothing -> string {
|
||||||
|
if ($path | is-empty) or $path == "/" { return "" }
|
||||||
|
if (is-workspace-dir $path) { return $path }
|
||||||
|
let parent = ($path | path dirname)
|
||||||
|
if $parent == $path { return "" }
|
||||||
|
find-workspace-up $parent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the ncl-sync daemon if not already running.
|
||||||
|
# Workspace resolution priority:
|
||||||
|
# 1. $NCL_CACHE_DIR's parent (explicit override)
|
||||||
|
# 2. Walk up from PWD until a workspace root is found
|
||||||
|
# 3. get-active-workspace-details from user_config.yaml (if it's a valid workspace path)
|
||||||
|
# 4. skip (avoid watching HOME or random dirs)
|
||||||
|
export def ncl-sync-start []: nothing -> nothing {
|
||||||
|
if (ncl-sync-running) { return }
|
||||||
|
let bin = (ncl-sync-bin)
|
||||||
|
if not ($bin | path exists) { return }
|
||||||
|
|
||||||
|
let from_pwd = (find-workspace-up $env.PWD)
|
||||||
|
let details = (get-active-workspace-details)
|
||||||
|
let from_config = if ($details | is-not-empty) and ($details | get -o path | is-not-empty) {
|
||||||
|
$details.path
|
||||||
|
} else { "" }
|
||||||
|
|
||||||
|
let ws_path = if ($from_pwd | is-not-empty) {
|
||||||
|
$from_pwd
|
||||||
|
} else if (is-workspace-dir $from_config) {
|
||||||
|
$from_config
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ws_path | is-empty) {
|
||||||
|
print "→ ncl-sync: no workspace detected in PWD tree or user_config — skipping"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
let log_dir = ($home | path join ".provisioning" "logs")
|
||||||
|
(do { mkdir $log_dir } | ignore)
|
||||||
|
let log_file = ($log_dir | path join "ncl-sync.log")
|
||||||
|
|
||||||
|
let cmd = $"nohup ($bin) daemon --workspace \"($ws_path)\" >>\"($log_file)\" 2>&1 &"
|
||||||
|
(^sh -c $cmd | ignore)
|
||||||
|
sleep 500ms
|
||||||
|
print $"→ ncl-sync started \(workspace: ($ws_path)\)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Stop the ncl-sync daemon via PID file.
|
||||||
|
export def ncl-sync-stop []: nothing -> nothing {
|
||||||
|
let pid_file = (ncl-sync-pid-file)
|
||||||
|
if not ($pid_file | path exists) { return }
|
||||||
|
let pid = (open $pid_file | str trim)
|
||||||
|
if ($pid | is-not-empty) {
|
||||||
|
(do { ^kill ($pid | into int) } | complete | ignore)
|
||||||
|
}
|
||||||
|
(do { rm -f $pid_file } | ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Status record for ncl-sync daemon.
|
||||||
|
export def ncl-sync-status []: nothing -> record {
|
||||||
|
let running = (ncl-sync-running)
|
||||||
|
let pid_file = (ncl-sync-pid-file)
|
||||||
|
let pid = if ($pid_file | path exists) { open $pid_file | str trim } else { "" }
|
||||||
|
{
|
||||||
|
service: "ncl-sync",
|
||||||
|
running: $running,
|
||||||
|
pid: $pid,
|
||||||
|
pid_file: $pid_file,
|
||||||
|
}
|
||||||
|
}
|
||||||
611
nulib/lib_provisioning/platform/startup.nu
Normal file
611
nulib/lib_provisioning/platform/startup.nu
Normal file
|
|
@ -0,0 +1,611 @@
|
||||||
|
# Platform Service Startup Management
|
||||||
|
# Provides service lifecycle management for local binary deployment mode
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
# - Service registry with metadata and dependencies
|
||||||
|
# - Service discovery (port availability, running status)
|
||||||
|
# - Startup orchestration with dependency resolution
|
||||||
|
# - Health checking and status reporting
|
||||||
|
|
||||||
|
# Color constants for terminal output
|
||||||
|
const COLOR_RESET = "\u{1b}[0m"
|
||||||
|
const COLOR_GREEN = "\u{1b}[32m"
|
||||||
|
const COLOR_YELLOW = "\u{1b}[33m"
|
||||||
|
const COLOR_RED = "\u{1b}[31m"
|
||||||
|
const COLOR_BLUE = "\u{1b}[34m"
|
||||||
|
const COLOR_CYAN = "\u{1b}[36m"
|
||||||
|
|
||||||
|
# Service registry with metadata
|
||||||
|
# Each service defines port, protocol, description, dependencies, and binary name
|
||||||
|
const SERVICES_REGISTRY = {
|
||||||
|
"vault-service": {
|
||||||
|
port: 8081,
|
||||||
|
protocol: "gRPC",
|
||||||
|
description: "Key management and encryption service",
|
||||||
|
depends_on: [],
|
||||||
|
binary: "vault-service"
|
||||||
|
},
|
||||||
|
"extension-registry": {
|
||||||
|
port: 8082,
|
||||||
|
protocol: "HTTP",
|
||||||
|
description: "OCI container registry for extensions",
|
||||||
|
depends_on: [],
|
||||||
|
binary: "extension-registry"
|
||||||
|
},
|
||||||
|
"control-center": {
|
||||||
|
port: 8000,
|
||||||
|
protocol: "HTTP/WebSocket",
|
||||||
|
description: "Core control plane with JWT auth",
|
||||||
|
depends_on: ["vault-service"],
|
||||||
|
binary: "control-center"
|
||||||
|
},
|
||||||
|
"provisioning-rag": {
|
||||||
|
port: 8300,
|
||||||
|
protocol: "REST",
|
||||||
|
description: "Vector search and RAG database",
|
||||||
|
depends_on: [],
|
||||||
|
binary: "provisioning-rag"
|
||||||
|
},
|
||||||
|
"ai-service": {
|
||||||
|
port: 8083,
|
||||||
|
protocol: "HTTP",
|
||||||
|
description: "AI service with RAG and MCP tools",
|
||||||
|
depends_on: ["provisioning-rag", "vault-service"],
|
||||||
|
binary: "ai-service"
|
||||||
|
},
|
||||||
|
"mcp-server": {
|
||||||
|
port: 8400,
|
||||||
|
protocol: "Binary",
|
||||||
|
description: "Infrastructure automation server",
|
||||||
|
depends_on: ["vault-service"],
|
||||||
|
binary: "mcp-server"
|
||||||
|
},
|
||||||
|
"provisioning-daemon": {
|
||||||
|
port: 8100,
|
||||||
|
protocol: "gRPC",
|
||||||
|
description: "Nushell script execution daemon",
|
||||||
|
depends_on: ["vault-service"],
|
||||||
|
binary: "provisioning-daemon"
|
||||||
|
},
|
||||||
|
"orchestrator": {
|
||||||
|
port: 9090,
|
||||||
|
protocol: "HTTP",
|
||||||
|
description: "Batch workflow orchestrator",
|
||||||
|
depends_on: ["extension-registry", "control-center", "ai-service"],
|
||||||
|
binary: "orchestrator"
|
||||||
|
},
|
||||||
|
"detector": {
|
||||||
|
port: 8600,
|
||||||
|
protocol: "HTTP",
|
||||||
|
description: "Infrastructure detection service",
|
||||||
|
depends_on: ["vault-service"],
|
||||||
|
binary: "detector"
|
||||||
|
},
|
||||||
|
"control-center-ui": {
|
||||||
|
port: 3000,
|
||||||
|
protocol: "HTTP (WASM)",
|
||||||
|
description: "Web UI dashboard (Leptos/WASM)",
|
||||||
|
depends_on: ["control-center"],
|
||||||
|
binary: "control-center-ui"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service group definitions for convenient selection
|
||||||
|
const SERVICE_GROUPS = {
|
||||||
|
"core": ["vault-service", "extension-registry", "control-center"],
|
||||||
|
"all": [
|
||||||
|
"vault-service",
|
||||||
|
"extension-registry",
|
||||||
|
"control-center",
|
||||||
|
"provisioning-rag",
|
||||||
|
"ai-service",
|
||||||
|
"mcp-server",
|
||||||
|
"provisioning-daemon",
|
||||||
|
"orchestrator",
|
||||||
|
"detector",
|
||||||
|
"control-center-ui"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Logging Utilities
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def log_info [message: string] {
|
||||||
|
print $"($COLOR_BLUE)ℹ($COLOR_RESET) ($message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_success [message: string] {
|
||||||
|
print $"($COLOR_GREEN)✓($COLOR_RESET) ($message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_warning [message: string] {
|
||||||
|
print $"($COLOR_YELLOW)⚠($COLOR_RESET) ($message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_error [message: string] {
|
||||||
|
print $"($COLOR_RED)✗($COLOR_RESET) ($message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def log_section [title: string] {
|
||||||
|
print $"($COLOR_CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━($COLOR_RESET)"
|
||||||
|
print $"($COLOR_CYAN)($title)($COLOR_RESET)"
|
||||||
|
print $"($COLOR_CYAN)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━($COLOR_RESET)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Service Discovery Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Check if a port is available (not listening)
|
||||||
|
export def is_port_available [port: int] {
|
||||||
|
# Simplified: assume available (actual port checking requires complex shell logic)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a service is currently running (port responding)
|
||||||
|
export def is_service_running [service_name: string] {
|
||||||
|
# Simplified: assume not running (actual port checking requires complex shell logic)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Probe TCP connectivity to a host:port
|
||||||
|
# Returns true if connection succeeds, false otherwise
|
||||||
|
export def probe_tcp [host: string, port: int] {
|
||||||
|
let result = (do {
|
||||||
|
^nc -zv -w 2 $host $port
|
||||||
|
} | complete)
|
||||||
|
$result.exit_code == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Probe HTTP endpoint (GET request)
|
||||||
|
# Returns true if HTTP 200-399 or 401 (authenticated), false otherwise
|
||||||
|
export def probe_http [url: string] {
|
||||||
|
let result = (do {
|
||||||
|
curl -s -f -m 5 -o /dev/null -w "%{http_code}" $url
|
||||||
|
} | complete)
|
||||||
|
|
||||||
|
if $result.exit_code != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract HTTP code from stdout (convert to int, default 0 if fails)
|
||||||
|
let status_str = ($result.stdout | str trim)
|
||||||
|
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||||||
|
($status_code >= 200 and $status_code <= 399) or ($status_code == 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Probe OCI registry v2 endpoint
|
||||||
|
# Zot/Harbor respond with 200, 401, or 404 (all mean the registry is there)
|
||||||
|
export def probe_oci_registry [registry_url: string] {
|
||||||
|
let v2_url = if ($registry_url | str contains "://") {
|
||||||
|
$"($registry_url)/v2/"
|
||||||
|
} else {
|
||||||
|
$"http://($registry_url)/v2/"
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = (do {
|
||||||
|
curl -s -m 5 -o /dev/null -w "%{http_code}" $v2_url
|
||||||
|
} | complete)
|
||||||
|
|
||||||
|
if $result.exit_code != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract HTTP code (fallback to 0 if not a valid integer)
|
||||||
|
let status_str = ($result.stdout | str trim)
|
||||||
|
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||||||
|
# 200 = OK, 401 = auth required (both good), 404 = registry exists but empty
|
||||||
|
($status_code >= 200 and $status_code <= 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Probe Git API (Gitea/Forgejo/GitHub)
|
||||||
|
# Checks if the Git service API is responding
|
||||||
|
export def probe_git_source [url: string, provider: string] {
|
||||||
|
let api_url = match $provider {
|
||||||
|
"github" => "https://api.github.com/zen"
|
||||||
|
"gitea" | "forgejo" => {
|
||||||
|
if ($url | str contains "://") {
|
||||||
|
$"($url)/api/v1/version"
|
||||||
|
} else {
|
||||||
|
$"http://($url)/api/v1/version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => $url
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = (do {
|
||||||
|
curl -s -m 5 -o /dev/null -w "%{http_code}" $api_url
|
||||||
|
} | complete)
|
||||||
|
|
||||||
|
if $result.exit_code != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract HTTP code (fallback to 0 if not a valid integer)
|
||||||
|
let status_str = ($result.stdout | str trim)
|
||||||
|
let status_code = (if ($status_str | is-empty) { 0 } else { ($status_str | into int) })
|
||||||
|
$status_code == 200
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all service names from registry
|
||||||
|
export def list_all_services [] {
|
||||||
|
$SERVICES_REGISTRY | columns
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get services in a group (core, all, or list)
|
||||||
|
export def get_services_for_group [group: string, custom_list: list<string>] {
|
||||||
|
if $group == "custom" {
|
||||||
|
$custom_list
|
||||||
|
} else if $group == "all" {
|
||||||
|
$SERVICE_GROUPS.all
|
||||||
|
} else if $group == "core" {
|
||||||
|
$SERVICE_GROUPS.core
|
||||||
|
} else {
|
||||||
|
$SERVICE_GROUPS.core
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get service info from registry
|
||||||
|
export def get_service_info [service_name: string] {
|
||||||
|
$SERVICES_REGISTRY | get $service_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Dependency Resolution
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Resolve startup order respecting service dependencies
|
||||||
|
# Returns ordered list or empty list if circular dependency detected
|
||||||
|
export def resolve_startup_order [services: list<string>] {
|
||||||
|
def can_start [service: string, ordered: list<string>] {
|
||||||
|
let deps = ($SERVICES_REGISTRY | get $service).depends_on
|
||||||
|
$deps | all { |dep| $ordered | any { |s| $s == $dep } }
|
||||||
|
}
|
||||||
|
|
||||||
|
def resolve_recursive [ordered: list<string>, remaining: list<string>, iterations: int] {
|
||||||
|
if ($iterations > 100) or (($remaining | length) == 0) {
|
||||||
|
if ($remaining | length) > 0 {
|
||||||
|
log_error $"Failed to resolve startup order for: ($remaining | str join ', ')"
|
||||||
|
[]
|
||||||
|
} else {
|
||||||
|
$ordered
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let startable = (
|
||||||
|
$remaining | where { |service| can_start $service $ordered }
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($startable | length) > 0 {
|
||||||
|
let service = $startable | get 0
|
||||||
|
let new_remaining = $remaining | where { |s| $s != $service }
|
||||||
|
resolve_recursive ($ordered | append $service) $new_remaining ($iterations + 1)
|
||||||
|
} else {
|
||||||
|
log_error $"Circular dependency detected or missing dependencies for: ($remaining | str join ', ')"
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_recursive [] $services 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Service Lifecycle Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Perform health check on a service
|
||||||
|
export def health_check [service_name: string] {
|
||||||
|
# Simplified: assume unhealthy (actual health checking requires curl support)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check all external services declared in config
|
||||||
|
# Returns record with overall status and per-service details
|
||||||
|
export def check_external_services [external_config: record] {
|
||||||
|
mut results = []
|
||||||
|
mut all_healthy = true
|
||||||
|
|
||||||
|
# Check database
|
||||||
|
let db = $external_config.database
|
||||||
|
let db_check = (match $db.backend {
|
||||||
|
"filesystem" | "rocksdb" => {
|
||||||
|
let path = $db.path? | default "~/.provisioning/data"
|
||||||
|
let expanded_path = if ($path | str starts-with "~") {
|
||||||
|
$"($env.HOME)/($path | str substring 1..)"
|
||||||
|
} else {
|
||||||
|
$path
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expanded_path | path exists) {
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "✓"
|
||||||
|
message: $"Filesystem storage available at ($expanded_path)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let parent = ($expanded_path | path dirname)
|
||||||
|
if ($parent | path exists) {
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "⚠"
|
||||||
|
message: $"Path does not exist but parent is writable: ($expanded_path)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
($all_healthy = false)
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "✗"
|
||||||
|
message: $"Path and parent do not exist: ($expanded_path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"surrealdb_server" => {
|
||||||
|
let conn_str = $db.connection_string? | default "ws://localhost:8000"
|
||||||
|
let host_port = (
|
||||||
|
$conn_str
|
||||||
|
| str replace "^ws://" ""
|
||||||
|
| str replace "^wss://" ""
|
||||||
|
| str replace "^http://" ""
|
||||||
|
| str replace "^https://" ""
|
||||||
|
)
|
||||||
|
let parts = ($host_port | split row ":" | take 2)
|
||||||
|
let host = $parts.0
|
||||||
|
let port = if ($parts | length) > 1 { (if ($parts.1 | is-empty) { 8000 } else { ($parts.1 | into int) }) } else { 8000 }
|
||||||
|
|
||||||
|
if (probe_tcp $host $port) {
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: "surrealdb_server"
|
||||||
|
status: "✓"
|
||||||
|
message: $"Connected to SurrealDB at ($host):($port)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
all_healthy = false
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: "surrealdb_server"
|
||||||
|
status: "✗"
|
||||||
|
message: $"Cannot reach SurrealDB at ($host):($port)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
all_healthy = false
|
||||||
|
{
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "✗"
|
||||||
|
message: $"Unknown database backend: ($db.backend)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$results = ($results | append $db_check)
|
||||||
|
|
||||||
|
# Check OCI registries
|
||||||
|
let oci_registries = $external_config.oci_registries? | default []
|
||||||
|
for oci in $oci_registries {
|
||||||
|
let id = $oci.id? | default "oci"
|
||||||
|
let registry = $oci.registry
|
||||||
|
|
||||||
|
if (probe_oci_registry $registry) {
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "oci_registry"
|
||||||
|
id: $id
|
||||||
|
registry: $registry
|
||||||
|
status: "✓"
|
||||||
|
message: $"OCI registry reachable at ($registry)"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
all_healthy = false
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "oci_registry"
|
||||||
|
id: $id
|
||||||
|
registry: $registry
|
||||||
|
status: "✗"
|
||||||
|
message: $"Cannot reach OCI registry at ($registry)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Git sources
|
||||||
|
let git_sources = $external_config.git_sources? | default []
|
||||||
|
for git in $git_sources {
|
||||||
|
let id = $git.id? | default $git.provider
|
||||||
|
let provider = $git.provider
|
||||||
|
let url = $git.url? | default (match $provider {
|
||||||
|
"github" => "github.com"
|
||||||
|
_ => "localhost:3000"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check if token file exists
|
||||||
|
let token_path = $git.token_path
|
||||||
|
let expanded_token = if ($token_path | str starts-with "~") {
|
||||||
|
$"($env.HOME)/($token_path | str substring 1..)"
|
||||||
|
} else {
|
||||||
|
$token_path
|
||||||
|
}
|
||||||
|
|
||||||
|
if not ($expanded_token | path exists) {
|
||||||
|
all_healthy = false
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "git_source"
|
||||||
|
id: $id
|
||||||
|
provider: $provider
|
||||||
|
url: $url
|
||||||
|
status: "✗"
|
||||||
|
message: $"Token file not found: ($token_path)"
|
||||||
|
})
|
||||||
|
} else if (probe_git_source $url $provider) {
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "git_source"
|
||||||
|
id: $id
|
||||||
|
provider: $provider
|
||||||
|
url: $url
|
||||||
|
status: "✓"
|
||||||
|
message: $"($provider) source reachable at ($url)"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
all_healthy = false
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "git_source"
|
||||||
|
id: $id
|
||||||
|
provider: $provider
|
||||||
|
url: $url
|
||||||
|
status: "✗"
|
||||||
|
message: $"Cannot reach ($provider) at ($url)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
let cache = $external_config.cache
|
||||||
|
let cache_check = (match $cache.mode {
|
||||||
|
"local" => {
|
||||||
|
let path = $cache.path? | default "~/.provisioning/oci-cache"
|
||||||
|
let expanded_path = if ($path | str starts-with "~") {
|
||||||
|
$"($env.HOME)/($path | str substring 1..)"
|
||||||
|
} else {
|
||||||
|
$path
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expanded_path | path exists) {
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: "local"
|
||||||
|
status: "✓"
|
||||||
|
message: $"Cache directory available at ($expanded_path)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let parent = ($expanded_path | path dirname)
|
||||||
|
if ($parent | path exists) {
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: "local"
|
||||||
|
status: "⚠"
|
||||||
|
message: $"Cache path does not exist but parent is writable: ($expanded_path)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
all_healthy = false
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: "local"
|
||||||
|
status: "✗"
|
||||||
|
message: $"Cache path parent does not exist: ($expanded_path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"remote" => {
|
||||||
|
let url = $cache.url? | default "redis://localhost:6379"
|
||||||
|
let host_port = (
|
||||||
|
$url
|
||||||
|
| str replace "^redis://" ""
|
||||||
|
| str replace "^rediss://" ""
|
||||||
|
)
|
||||||
|
let parts = ($host_port | split row ":" | take 2)
|
||||||
|
let host = $parts.0
|
||||||
|
let port = if ($parts | length) > 1 { ($parts.1 | into int? | default 6379) } else { 6379 }
|
||||||
|
|
||||||
|
if (probe_tcp $host $port) {
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: "remote"
|
||||||
|
status: "✓"
|
||||||
|
message: $"Cache service reachable at ($host):($port)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
all_healthy = false
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: "remote"
|
||||||
|
status: "✗"
|
||||||
|
message: $"Cannot reach cache service at ($host):($port)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
all_healthy = false
|
||||||
|
{
|
||||||
|
service: "cache"
|
||||||
|
mode: $cache.mode
|
||||||
|
status: "✗"
|
||||||
|
message: $"Unknown cache mode: ($cache.mode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$results = ($results | append $cache_check)
|
||||||
|
|
||||||
|
{
|
||||||
|
all_healthy: $all_healthy
|
||||||
|
services: $results
|
||||||
|
timestamp: (date now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Status Reporting
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Display status of services
|
||||||
|
export def show_status [services: list<string>] {
|
||||||
|
log_section "Service Status"
|
||||||
|
|
||||||
|
for service in $services {
|
||||||
|
let service_info = ($SERVICES_REGISTRY | get $service)
|
||||||
|
let is_running = (is_service_running $service)
|
||||||
|
let status = (if $is_running { $"($COLOR_GREEN)✓ RUNNING($COLOR_RESET)" } else { $"($COLOR_RED)✗ STOPPED($COLOR_RESET)" })
|
||||||
|
let port = $service_info.port
|
||||||
|
|
||||||
|
print $"($service): $status (port $port)"
|
||||||
|
}
|
||||||
|
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display service URLs for a list of started services
|
||||||
|
export def show_service_urls [services: list<string>] {
|
||||||
|
log_info "Service URLs:"
|
||||||
|
|
||||||
|
for service in $services {
|
||||||
|
let service_info = ($SERVICES_REGISTRY | get $service)
|
||||||
|
let port = $service_info.port
|
||||||
|
let protocol = if (($port == 8081) or ($port == 8100)) { "grpc://" } else { "http://" }
|
||||||
|
print $" ($service): ($protocol)localhost:($port)"
|
||||||
|
}
|
||||||
|
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configuration Parsing
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get services to start based on configuration
|
||||||
|
export def get_services_to_start [services_set: string, custom_services: list<string>] {
|
||||||
|
if $services_set == "custom" {
|
||||||
|
$custom_services
|
||||||
|
} else if $services_set == "all" {
|
||||||
|
$SERVICE_GROUPS.all
|
||||||
|
} else {
|
||||||
|
$SERVICE_GROUPS.core
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate that all requested services exist in registry
|
||||||
|
export def validate_services [services: list<string>] {
|
||||||
|
let all_services = list_all_services
|
||||||
|
let invalid = $services | where { |s| $s not-in $all_services }
|
||||||
|
|
||||||
|
if ($invalid | length) > 0 {
|
||||||
|
print $"Error: Unknown services: ($invalid | str join ', ')"
|
||||||
|
print $"Available services: ($all_services | str join ', ')"
|
||||||
|
[]
|
||||||
|
} else {
|
||||||
|
$services
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,178 +1,164 @@
|
||||||
# Platform Target Configuration System
|
# Platform Target Configuration System
|
||||||
# Loads and manages platform service configurations for workspaces
|
|
||||||
|
|
||||||
use ../user/config.nu *
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
# Load platform target configuration for active workspace
|
# Get deployment configuration directory
|
||||||
export def load-platform-target [] {
|
def get-config-dir [] {
|
||||||
let workspace = (get-active-workspace)
|
if ($nu.os-info.name == "macos") {
|
||||||
|
$"($env.HOME)/Library/Application Support/provisioning/platform"
|
||||||
if ($workspace | is-empty) {
|
} else {
|
||||||
error make {
|
$"($env.HOME)/.config/provisioning/platform"
|
||||||
msg: "No active workspace. Run: provisioning workspace activate <name>"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_file = ([
|
|
||||||
$workspace
|
|
||||||
"config"
|
|
||||||
"platform"
|
|
||||||
"target.yaml"
|
|
||||||
] | path join)
|
|
||||||
|
|
||||||
if not ($target_file | path exists) {
|
|
||||||
# Return default platform target
|
|
||||||
return (get-default-platform-target $workspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Open and parse the YAML file directly
|
|
||||||
open $target_file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get default platform target for a workspace
|
# Load deployment configuration
|
||||||
export def get-default-platform-target [workspace_name: string] {
|
export def load-deployment-mode [] {
|
||||||
|
let config_dir = (get-config-dir)
|
||||||
|
let config_file = $"($config_dir)/deployment-mode.ncl"
|
||||||
|
|
||||||
|
if not ($config_file | path exists) {
|
||||||
|
print $"ERROR: Configuration file not found at ($config_file)"
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let import_path = ($env.PROVISIONING? | default "")
|
||||||
|
let import_paths = if ($import_path | is-not-empty) { [$import_path] } else { [] }
|
||||||
|
let content = (ncl-eval-soft $config_file $import_paths null)
|
||||||
|
|
||||||
|
if $content != null {
|
||||||
|
$content
|
||||||
|
} else {
|
||||||
|
print "ERROR: Failed to export Nickel configuration"
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get enabled services
|
||||||
|
export def get-enabled-services [] {
|
||||||
|
let deployment = (load-deployment-mode)
|
||||||
|
|
||||||
|
if not ("services" in $deployment) {
|
||||||
|
print "ERROR: No services found in deployment configuration"
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let services = $deployment.services
|
||||||
|
|
||||||
|
let all_services = ($services | columns)
|
||||||
|
|
||||||
|
# Filter only enabled services
|
||||||
|
let enabled = (
|
||||||
|
$all_services
|
||||||
|
| where {|key|
|
||||||
|
let svc = ($services | get $key)
|
||||||
|
let is_enabled = ($svc.enabled? | default false)
|
||||||
|
$is_enabled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
$enabled
|
||||||
|
| each {|name|
|
||||||
|
let cfg = $services | get $name
|
||||||
|
let priority = ($cfg.priority? | default 999)
|
||||||
|
{name: $name, config: $cfg, priority: $priority}
|
||||||
|
}
|
||||||
|
| sort-by priority
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get single service config
|
||||||
|
export def get-deployment-service-config [service: string] {
|
||||||
|
let deployment = (load-deployment-mode)
|
||||||
|
$deployment.services | get $service
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get default target
|
||||||
|
export def get-default-platform-target [workspace: string] {
|
||||||
{
|
{
|
||||||
platform: {
|
platform: {
|
||||||
name: $"($workspace_name)-local-dev"
|
name: $"($workspace)-local"
|
||||||
type: "local"
|
type: "local"
|
||||||
mode: "development"
|
mode: "development"
|
||||||
services: {
|
services: {
|
||||||
orchestrator: {
|
orchestrator: {
|
||||||
enabled: true
|
enabled: true
|
||||||
endpoint: "http://localhost:9090"
|
endpoint: "http://localhost:9090"
|
||||||
deployment_mode: "binary"
|
|
||||||
auto_start: true
|
|
||||||
required: true
|
required: true
|
||||||
data_dir: ".orchestrator"
|
health_check: {endpoint: "/health", timeout: 5000}
|
||||||
health_check: { endpoint: "/health", timeout_ms: 5000 }
|
|
||||||
}
|
}
|
||||||
control-center: {
|
control: {
|
||||||
enabled: false
|
enabled: false
|
||||||
endpoint: "http://localhost:9080"
|
endpoint: "http://localhost:9080"
|
||||||
deployment_mode: "binary"
|
|
||||||
auto_start: false
|
|
||||||
required: false
|
required: false
|
||||||
health_check: { endpoint: "/health", timeout_ms: 5000 }
|
health_check: {endpoint: "/health", timeout: 5000}
|
||||||
}
|
}
|
||||||
kms-service: {
|
kms: {
|
||||||
enabled: true
|
enabled: true
|
||||||
endpoint: "http://localhost:8090"
|
endpoint: "http://localhost:8090"
|
||||||
deployment_mode: "binary"
|
|
||||||
auto_start: true
|
|
||||||
required: true
|
required: true
|
||||||
backend: "age"
|
health_check: {endpoint: "/health", timeout: 5000}
|
||||||
health_check: { endpoint: "/health", timeout_ms: 5000 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate platform target configuration
|
# Validate target
|
||||||
export def validate-platform-target [target: record] {
|
export def validate-platform-target [target: record] {
|
||||||
if ($target == null) {
|
("platform" in $target)
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("platform" not-in $target) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let platform = $target.platform
|
|
||||||
|
|
||||||
if ("name" not-in $platform or "type" not-in $platform or "mode" not-in $platform) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("services" not-in $platform) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get platform endpoint for a service
|
# Detect mode from endpoint
|
||||||
|
export def detect-platform-mode [endpoint: string] {
|
||||||
|
if ($endpoint =~ "localhost") {
|
||||||
|
"local"
|
||||||
|
} else {
|
||||||
|
"remote"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if service should start locally
|
||||||
|
export def should-start-locally [config: record] {
|
||||||
|
let mode = (detect-platform-mode $config.endpoint)
|
||||||
|
($mode == "local")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get endpoint — builds URL from server.{host,port} if no explicit endpoint field.
|
||||||
export def get-platform-endpoint [service: string] {
|
export def get-platform-endpoint [service: string] {
|
||||||
let platform = (load-platform-target)
|
let cfg = (get-deployment-service-config $service)
|
||||||
|
let explicit = ($cfg | get -o endpoint | default "")
|
||||||
if $service not-in $platform.platform.services {
|
if ($explicit | is-not-empty) {
|
||||||
error make { msg: $"Unknown service: ($service)" }
|
$explicit
|
||||||
}
|
} else {
|
||||||
|
let srv = ($cfg | get -o server)
|
||||||
let svc = $platform.platform.services | get $service
|
if $srv == null {
|
||||||
|
""
|
||||||
if not $svc.enabled {
|
} else {
|
||||||
error make { msg: $"Service ($service) not enabled in platform target" }
|
let host = ($srv | get -o host | default "127.0.0.1")
|
||||||
}
|
let port = ($srv | get -o port | default 0)
|
||||||
|
if $port == 0 { "" } else { $"http://($host):($port)" }
|
||||||
$svc.endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check if platform service is enabled
|
|
||||||
export def is-platform-service-enabled [service: string] {
|
|
||||||
let platform = (load-platform-target)
|
|
||||||
|
|
||||||
if $service not-in $platform.platform.services {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
($platform.platform.services | get $service).enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get full platform service configuration
|
|
||||||
export def get-platform-service-config [service: string] {
|
|
||||||
let platform = (load-platform-target)
|
|
||||||
|
|
||||||
if $service not-in $platform.platform.services {
|
|
||||||
error make { msg: $"Unknown service: ($service)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$platform.platform.services | get $service
|
|
||||||
}
|
|
||||||
|
|
||||||
# List all enabled platform services
|
|
||||||
export def list-enabled-platform-services [] {
|
|
||||||
let platform = (load-platform-target)
|
|
||||||
let services = $platform.platform.services
|
|
||||||
|
|
||||||
$services
|
|
||||||
| columns
|
|
||||||
| where {|svc| ($services | get $svc).enabled }
|
|
||||||
}
|
|
||||||
|
|
||||||
# List all required platform services
|
|
||||||
export def list-required-platform-services [] {
|
|
||||||
let platform = (load-platform-target)
|
|
||||||
let services = $platform.platform.services
|
|
||||||
let service_names = ($services | columns)
|
|
||||||
|
|
||||||
# Build list of required services
|
|
||||||
mut result = []
|
|
||||||
for svc in $service_names {
|
|
||||||
let config = ($services | get $svc)
|
|
||||||
if ($config.enabled) and ($config.required) {
|
|
||||||
$result = ($result | append {
|
|
||||||
name: $svc
|
|
||||||
config: $config
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect platform deployment mode from endpoint
|
# Check if enabled
|
||||||
export def detect-platform-mode [endpoint: string] {
|
export def is-platform-service-enabled [service: string] {
|
||||||
if $endpoint =~ "^https?://localhost" or $endpoint =~ "^https?://127\\.0\\.0\\.1" {
|
let cfg = (get-deployment-service-config $service)
|
||||||
"local"
|
$cfg.enabled
|
||||||
} else if $endpoint =~ "^https?://" {
|
|
||||||
"remote"
|
|
||||||
} else {
|
|
||||||
"local"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if service should be started locally
|
# Get config
|
||||||
export def should-start-locally [service_config: record] {
|
export def get-platform-service-config [service: string] {
|
||||||
let mode = (detect-platform-mode $service_config.endpoint)
|
get-deployment-service-config $service
|
||||||
$mode == "local" and ($service_config.deployment_mode? | default "binary") != "remote"
|
}
|
||||||
|
|
||||||
|
# List enabled
|
||||||
|
export def list-enabled-platform-services [] {
|
||||||
|
get-enabled-services | each {|s| {name: $s.name}}
|
||||||
|
}
|
||||||
|
|
||||||
|
# List required
|
||||||
|
export def list-required-platform-services [] {
|
||||||
|
get-enabled-services
|
||||||
|
| where {|s| ($s.config.required? | default false)}
|
||||||
|
| each {|s| {name: $s.name}}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
# Module: Authentication Plugin
|
# Module: Authentication Plugin
|
||||||
# Purpose: Provides JWT authentication, MFA enrollment/verification, auth status checking, and permission validation.
|
# Purpose: Provides JWT authentication, MFA enrollment/verification, auth status checking, and permission validation.
|
||||||
# Dependencies: std log
|
# Dependencies: std log, path-utils, auth_impl
|
||||||
|
|
||||||
|
use ../config/accessor.nu *
|
||||||
|
use ../utils/path-utils.nu *
|
||||||
|
export use auth_impl.nu *
|
||||||
|
|
||||||
|
# Check if Auth plugin is available (registered with Nushell)
|
||||||
|
def is-plugin-available [] {
|
||||||
|
let installed = (version | get installed_plugins)
|
||||||
|
$installed | str contains "auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if Auth plugin is enabled in config
|
||||||
|
def is-plugin-enabled [] {
|
||||||
|
config-get "plugins.auth_enabled" true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get Auth plugin status and configuration
|
||||||
|
export def plugin-auth-status [] {
|
||||||
|
let plugin_available = is-plugin-available
|
||||||
|
let plugin_enabled = is-plugin-enabled
|
||||||
|
|
||||||
|
{
|
||||||
|
plugin_available: $plugin_available
|
||||||
|
plugin_enabled: $plugin_enabled
|
||||||
|
mode: (if ($plugin_enabled and $plugin_available) { "plugin" } else { "disabled" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
use ../commands/traits.nu *
|
use ../commands/traits.nu *
|
||||||
|
|
||||||
# Check if auth plugin is available
|
# Check if auth plugin is available (registered with Nushell)
|
||||||
|
|
||||||
# Import implementation module
|
|
||||||
use ./auth_impl.nu *
|
|
||||||
|
|
||||||
def is-plugin-available [] {
|
def is-plugin-available [] {
|
||||||
(which auth | length) > 0
|
let installed = (version | get installed_plugins)
|
||||||
|
$installed | str contains "auth"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if auth plugin is enabled in config
|
# Check if auth plugin is enabled in config
|
||||||
|
|
@ -36,7 +33,9 @@ def store-token-keyring [
|
||||||
token: string
|
token: string
|
||||||
] {
|
] {
|
||||||
if (is-plugin-available) {
|
if (is-plugin-available) {
|
||||||
auth store-token $token
|
# Note: auth plugin doesn't provide store-token command
|
||||||
|
# Token storage is handled by the auth service backend
|
||||||
|
print "⚠️ Token storage via keyring requires authentication service"
|
||||||
} else {
|
} else {
|
||||||
print "⚠️ Keyring storage unavailable (plugin not loaded)"
|
print "⚠️ Keyring storage unavailable (plugin not loaded)"
|
||||||
}
|
}
|
||||||
|
|
@ -44,11 +43,9 @@ def store-token-keyring [
|
||||||
|
|
||||||
# Retrieve token from OS keyring (requires plugin)
|
# Retrieve token from OS keyring (requires plugin)
|
||||||
def get-token-keyring [] {
|
def get-token-keyring [] {
|
||||||
if (is-plugin-available) {
|
# Token retrieval from keyring not implemented in current auth plugin
|
||||||
auth get-token
|
# Check environment variable as fallback
|
||||||
} else {
|
$env.PROVISIONING_AUTH_TOKEN? | default ""
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper to safely execute a closure and return null on error
|
# Helper to safely execute a closure and return null on error
|
||||||
|
|
@ -93,7 +90,7 @@ export def plugin-login [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
let url = $"(get-control-center-url)/api/auth/login"
|
let url = $"(get-control-center-url)/api/auth/login"
|
||||||
|
|
||||||
let body = if ($mfa_code | is-empty) {
|
let body = if ($mfa_code | is-empty) {
|
||||||
|
|
@ -139,7 +136,7 @@ export def plugin-logout [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
let url = $"(get-control-center-url)/api/auth/logout"
|
let url = $"(get-control-center-url)/api/auth/logout"
|
||||||
|
|
||||||
let result = (do -i {
|
let result = (do -i {
|
||||||
|
|
@ -162,6 +159,7 @@ export def plugin-logout [] {
|
||||||
export def plugin-verify [] {
|
export def plugin-verify [] {
|
||||||
let enabled = is-plugin-enabled
|
let enabled = is-plugin-enabled
|
||||||
let available = is-plugin-available
|
let available = is-plugin-available
|
||||||
|
let environment = (config-get "environment" "dev")
|
||||||
|
|
||||||
if $enabled and $available {
|
if $enabled and $available {
|
||||||
let plugin_result = (try-plugin {
|
let plugin_result = (try-plugin {
|
||||||
|
|
@ -172,11 +170,16 @@ export def plugin-verify [] {
|
||||||
return $plugin_result
|
return $plugin_result
|
||||||
}
|
}
|
||||||
|
|
||||||
print "⚠️ Plugin verify failed, falling back to HTTP"
|
# Only show warning if not in dev mode
|
||||||
|
if $environment != "dev" {
|
||||||
|
print "⚠️ Plugin verify failed, falling back to HTTP"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback - only show warning if not in dev mode
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
if $environment != "dev" {
|
||||||
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
|
}
|
||||||
let token = get-token-keyring
|
let token = get-token-keyring
|
||||||
|
|
||||||
if ($token | is-empty) {
|
if ($token | is-empty) {
|
||||||
|
|
@ -215,7 +218,7 @@ export def plugin-sessions [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
let token = get-token-keyring
|
let token = get-token-keyring
|
||||||
|
|
||||||
if ($token | is-empty) {
|
if ($token | is-empty) {
|
||||||
|
|
@ -256,7 +259,7 @@ export def plugin-mfa-enroll [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
let token = get-token-keyring
|
let token = get-token-keyring
|
||||||
|
|
||||||
if ($token | is-empty) {
|
if ($token | is-empty) {
|
||||||
|
|
@ -303,7 +306,7 @@ export def plugin-mfa-verify [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_auth not available - using HTTP fallback for authentication"
|
||||||
let token = get-token-keyring
|
let token = get-token-keyring
|
||||||
|
|
||||||
if ($token | is-empty) {
|
if ($token | is-empty) {
|
||||||
|
|
@ -452,3 +455,12 @@ def validate-permission-level [
|
||||||
# Determine auth enforcement based on metadata
|
# Determine auth enforcement based on metadata
|
||||||
export def should-enforce-auth-from-metadata [
|
export def should-enforce-auth-from-metadata [
|
||||||
command_name: string # Command to check
|
command_name: string # Command to check
|
||||||
|
] {
|
||||||
|
# Get metadata for command and check auth requirements
|
||||||
|
let metadata = (get-command-metadata $command_name)
|
||||||
|
if ($metadata | type) == "record" {
|
||||||
|
$metadata | get requirements.requires_auth? | default false
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,102 @@
|
||||||
|
# Module: Authentication Implementation Details
|
||||||
|
# Purpose: Internal auth functions for policy enforcement, metadata evaluation, and auth flows
|
||||||
|
# Dependencies: config/accessor, plugins/kms, commands/traits, auth_core
|
||||||
|
|
||||||
|
use ../config/accessor.nu *
|
||||||
|
use ../commands/traits.nu *
|
||||||
|
use auth_core.nu *
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Metadata-Driven Authentication Helpers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get auth requirements from metadata for a specific command
|
||||||
|
def get-metadata-auth-requirements [
|
||||||
|
command_name: string
|
||||||
|
] {
|
||||||
|
let metadata = (get-command-metadata $command_name)
|
||||||
|
|
||||||
|
if ($metadata | type) == "record" {
|
||||||
|
let requirements = ($metadata | get requirements? | default {})
|
||||||
|
{
|
||||||
|
requires_auth: ($requirements | get requires_auth? | default false)
|
||||||
|
auth_type: ($requirements | get auth_type? | default "none")
|
||||||
|
requires_confirmation: ($requirements | get requires_confirmation? | default false)
|
||||||
|
min_permission: ($requirements | get min_permission? | default "read")
|
||||||
|
side_effect_type: ($requirements | get side_effect_type? | default "none")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
requires_auth: false
|
||||||
|
auth_type: "none"
|
||||||
|
requires_confirmation: false
|
||||||
|
min_permission: "read"
|
||||||
|
side_effect_type: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if MFA is required based on metadata auth_type
|
||||||
|
def requires-mfa-from-metadata [
|
||||||
|
command_name: string
|
||||||
|
] {
|
||||||
|
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||||
|
$auth_reqs.auth_type == "mfa" or $auth_reqs.auth_type == "cedar"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if operation is destructive based on metadata
|
||||||
|
def is-destructive-from-metadata [
|
||||||
|
command_name: string
|
||||||
|
] {
|
||||||
|
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||||
|
$auth_reqs.side_effect_type == "delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine if operation is production-related
|
||||||
|
def is-production-from-metadata [
|
||||||
|
command_name: string
|
||||||
|
] {
|
||||||
|
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||||
|
$auth_reqs.auth_type == "mfa" or $auth_reqs.side_effect_type in ["delete" "modify"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate user has required permission level for operation
|
||||||
|
def validate-permission-level [
|
||||||
|
operation_name: string
|
||||||
|
user_level: string
|
||||||
|
] {
|
||||||
|
let auth_reqs = (get-metadata-auth-requirements $operation_name)
|
||||||
|
let min_perm = $auth_reqs.min_permission
|
||||||
|
|
||||||
|
# Permission level hierarchy
|
||||||
|
let req_level = (
|
||||||
|
if $min_perm == "read" { 0 }
|
||||||
|
else if $min_perm == "write" { 1 }
|
||||||
|
else if $min_perm == "admin" { 2 }
|
||||||
|
else if $min_perm == "superadmin" { 3 }
|
||||||
|
else { -1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get user permission level index
|
||||||
|
let usr_level = (
|
||||||
|
if $user_level == "read" { 0 }
|
||||||
|
else if $user_level == "write" { 1 }
|
||||||
|
else if $user_level == "admin" { 2 }
|
||||||
|
else if $user_level == "superadmin" { 3 }
|
||||||
|
else { -1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
# User must have equal or higher permission level
|
||||||
|
if $req_level < 0 or $usr_level < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
$usr_level >= $req_level
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine auth enforcement based on metadata
|
||||||
|
export def should-enforce-auth-from-metadata [
|
||||||
|
command_name: string
|
||||||
] {
|
] {
|
||||||
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||||
|
|
||||||
|
|
@ -61,86 +160,64 @@ export def get-authenticated-user [] {
|
||||||
|
|
||||||
# Require authentication with clear error messages
|
# Require authentication with clear error messages
|
||||||
export def require-auth [
|
export def require-auth [
|
||||||
operation: string # Operation name for error messages
|
operation: string
|
||||||
--allow-skip # Allow skip-auth flag bypass
|
--allow-skip
|
||||||
] {
|
] {
|
||||||
# Check if authentication is required
|
# Guard: Check if environment is dev (skip auth)
|
||||||
|
let environment = (config-get "environment" "dev")
|
||||||
|
if $environment == "dev" {
|
||||||
|
# Auth not required in dev environment
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if not (should-require-auth) {
|
if not (should-require-auth) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if skip is allowed
|
|
||||||
if $allow_skip and (($env.PROVISIONING_SKIP_AUTH? | default "false") == "true") {
|
if $allow_skip and (($env.PROVISIONING_SKIP_AUTH? | default "false") == "true") {
|
||||||
print $"⚠️ Authentication bypassed with PROVISIONING_SKIP_AUTH flag"
|
print $"⚠️ Authentication bypassed with PROVISIONING_SKIP_AUTH flag"
|
||||||
print $" (ansi yellow_bold)WARNING: This should only be used in development/testing!(ansi reset)"
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify authentication
|
|
||||||
let auth_status = (plugin-verify)
|
let auth_status = (plugin-verify)
|
||||||
|
|
||||||
if not ($auth_status | get valid? | default false) {
|
if not ($auth_status | get valid? | default false) {
|
||||||
print $"(ansi red_bold)❌ Authentication Required(ansi reset)"
|
print $"❌ Authentication Required"
|
||||||
print ""
|
print $"Operation: ($operation)"
|
||||||
print $"Operation: (ansi cyan_bold)($operation)(ansi reset)"
|
|
||||||
print $"You must be logged in to perform this operation."
|
|
||||||
print ""
|
|
||||||
print $"(ansi green_bold)To login:(ansi reset)"
|
|
||||||
print $" provisioning auth login <username>"
|
|
||||||
print ""
|
|
||||||
print $"(ansi yellow_bold)Note:(ansi reset) Your credentials will be securely stored in the system keyring."
|
|
||||||
|
|
||||||
if ($auth_status | get message? | default null | is-not-empty) {
|
|
||||||
print ""
|
|
||||||
print $"(ansi red)Error:(ansi reset) ($auth_status.message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
let username = ($auth_status | get username? | default "unknown")
|
let username = ($auth_status | get username? | default "unknown")
|
||||||
print $"(ansi green)✓(ansi reset) Authenticated as: (ansi cyan_bold)($username)(ansi reset)"
|
print $"✓ Authenticated as: ($username)"
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Require MFA verification with clear error messages
|
# Require MFA verification
|
||||||
export def require-mfa [
|
export def require-mfa [
|
||||||
operation: string # Operation name for error messages
|
operation: string
|
||||||
reason: string # Reason MFA is required
|
reason: string
|
||||||
] {
|
] {
|
||||||
let auth_status = (plugin-verify)
|
let auth_status = (plugin-verify)
|
||||||
|
|
||||||
if not ($auth_status | get mfa_verified? | default false) {
|
if not ($auth_status | get mfa_verified? | default false) {
|
||||||
print $"(ansi red_bold)❌ MFA Verification Required(ansi reset)"
|
print $"❌ MFA Verification Required"
|
||||||
print ""
|
print $"Operation: ($operation)"
|
||||||
print $"Operation: (ansi cyan_bold)($operation)(ansi reset)"
|
print $"Reason: ($reason)"
|
||||||
print $"Reason: (ansi yellow)($reason)(ansi reset)"
|
|
||||||
print ""
|
|
||||||
print $"(ansi green_bold)To verify MFA:(ansi reset)"
|
|
||||||
print $" 1. Get code from your authenticator app"
|
|
||||||
print $" 2. Run: provisioning auth mfa verify --code <6-digit-code>"
|
|
||||||
print ""
|
|
||||||
print $"(ansi yellow_bold)Don't have MFA set up?(ansi reset)"
|
|
||||||
print $" Run: provisioning auth mfa enroll totp"
|
|
||||||
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
print $"(ansi green)✓(ansi reset) MFA verified"
|
print $"✓ MFA verified"
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check authentication and MFA for production operations (enhanced with metadata)
|
# Check auth for production operations
|
||||||
export def check-auth-for-production [
|
export def check-auth-for-production [
|
||||||
operation: string # Operation name
|
operation: string
|
||||||
--allow-skip # Allow skip-auth flag bypass
|
--allow-skip
|
||||||
] {
|
] {
|
||||||
# First check if this command is actually production-related via metadata
|
|
||||||
if (is-production-from-metadata $operation) {
|
if (is-production-from-metadata $operation) {
|
||||||
# Require authentication first
|
|
||||||
require-auth $operation --allow-skip=$allow_skip
|
require-auth $operation --allow-skip=$allow_skip
|
||||||
|
|
||||||
# Check if MFA is required based on metadata or config
|
|
||||||
let requires_mfa_metadata = (requires-mfa-from-metadata $operation)
|
let requires_mfa_metadata = (requires-mfa-from-metadata $operation)
|
||||||
if $requires_mfa_metadata or (should-require-mfa-prod) {
|
if $requires_mfa_metadata or (should-require-mfa-prod) {
|
||||||
require-mfa $operation "production environment operation"
|
require-mfa $operation "production environment operation"
|
||||||
|
|
@ -149,7 +226,6 @@ export def check-auth-for-production [
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback to configuration-based check if not in metadata
|
|
||||||
if (should-require-mfa-prod) {
|
if (should-require-mfa-prod) {
|
||||||
require-auth $operation --allow-skip=$allow_skip
|
require-auth $operation --allow-skip=$allow_skip
|
||||||
require-mfa $operation "production environment operation"
|
require-mfa $operation "production environment operation"
|
||||||
|
|
@ -158,17 +234,14 @@ export def check-auth-for-production [
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check authentication and MFA for destructive operations (enhanced with metadata)
|
# Check auth for destructive operations
|
||||||
export def check-auth-for-destructive [
|
export def check-auth-for-destructive [
|
||||||
operation: string # Operation name
|
operation: string
|
||||||
--allow-skip # Allow skip-auth flag bypass
|
--allow-skip
|
||||||
] {
|
] {
|
||||||
# Check if this is a destructive operation via metadata
|
|
||||||
if (is-destructive-from-metadata $operation) {
|
if (is-destructive-from-metadata $operation) {
|
||||||
# Always require authentication for destructive ops
|
|
||||||
require-auth $operation --allow-skip=$allow_skip
|
require-auth $operation --allow-skip=$allow_skip
|
||||||
|
|
||||||
# Check if MFA is required based on metadata or config
|
|
||||||
let requires_mfa_metadata = (requires-mfa-from-metadata $operation)
|
let requires_mfa_metadata = (requires-mfa-from-metadata $operation)
|
||||||
if $requires_mfa_metadata or (should-require-mfa-destructive) {
|
if $requires_mfa_metadata or (should-require-mfa-destructive) {
|
||||||
require-mfa $operation "destructive operation (delete/destroy)"
|
require-mfa $operation "destructive operation (delete/destroy)"
|
||||||
|
|
@ -177,7 +250,6 @@ export def check-auth-for-destructive [
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback to configuration-based check
|
|
||||||
if (should-require-mfa-destructive) {
|
if (should-require-mfa-destructive) {
|
||||||
require-auth $operation --allow-skip=$allow_skip
|
require-auth $operation --allow-skip=$allow_skip
|
||||||
require-mfa $operation "destructive operation (delete/destroy)"
|
require-mfa $operation "destructive operation (delete/destroy)"
|
||||||
|
|
@ -186,7 +258,7 @@ export def check-auth-for-destructive [
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: Check if operation is in check mode (should skip auth)
|
# Helper: Check if operation is in check mode
|
||||||
export def is-check-mode [flags: record] {
|
export def is-check-mode [flags: record] {
|
||||||
(($flags | get check? | default false) or
|
(($flags | get check? | default false) or
|
||||||
($flags | get check_mode? | default false) or
|
($flags | get check_mode? | default false) or
|
||||||
|
|
@ -198,60 +270,44 @@ export def is-destructive-operation [operation_type: string] {
|
||||||
$operation_type in ["delete" "destroy" "remove"]
|
$operation_type in ["delete" "destroy" "remove"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main authentication check for any operation (enhanced with metadata)
|
# Main authentication check for any operation
|
||||||
export def check-operation-auth [
|
export def check-operation-auth [
|
||||||
operation_name: string # Name of operation
|
operation_name: string
|
||||||
operation_type: string # Type: create, delete, modify, read
|
operation_type: string
|
||||||
flags?: record # Command flags
|
flags?: record
|
||||||
] {
|
] {
|
||||||
# Skip in check mode
|
|
||||||
if ($flags | is-not-empty) and (is-check-mode $flags) {
|
if ($flags | is-not-empty) and (is-check-mode $flags) {
|
||||||
print $"(ansi dim)Skipping authentication check (check mode)(ansi reset)"
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check metadata-driven auth enforcement first
|
|
||||||
if (should-enforce-auth-from-metadata $operation_name) {
|
if (should-enforce-auth-from-metadata $operation_name) {
|
||||||
let auth_reqs = (get-metadata-auth-requirements $operation_name)
|
let auth_reqs = (get-metadata-auth-requirements $operation_name)
|
||||||
|
|
||||||
# Require authentication
|
|
||||||
let allow_skip = (config-get "security.bypass.allow_skip_auth" false)
|
let allow_skip = (config-get "security.bypass.allow_skip_auth" false)
|
||||||
require-auth $operation_name --allow-skip=$allow_skip
|
require-auth $operation_name --allow-skip=$allow_skip
|
||||||
|
|
||||||
# Check MFA based on auth_type from metadata
|
|
||||||
if $auth_reqs.auth_type == "mfa" {
|
if $auth_reqs.auth_type == "mfa" {
|
||||||
require-mfa $operation_name $"MFA required for ($operation_name)"
|
require-mfa $operation_name $"MFA required for ($operation_name)"
|
||||||
} else if $auth_reqs.auth_type == "cedar" {
|
|
||||||
# Cedar policy evaluation would go here
|
|
||||||
require-mfa $operation_name "Cedar policy verification required"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Validate permission level if set
|
|
||||||
let user_level = (config-get "security.user_permission_level" "read")
|
let user_level = (config-get "security.user_permission_level" "read")
|
||||||
if not (validate-permission-level $operation_name $user_level) {
|
if not (validate-permission-level $operation_name $user_level) {
|
||||||
print $"(ansi red_bold)❌ Insufficient Permissions(ansi reset)"
|
print $"❌ Insufficient Permissions"
|
||||||
print $"Operation: (ansi cyan)($operation_name)(ansi reset)"
|
|
||||||
print $"Required: (ansi yellow)($auth_reqs.min_permission)(ansi reset)"
|
|
||||||
print $"Your level: (ansi yellow)($user_level)(ansi reset)"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Skip if auth not required by configuration
|
|
||||||
if not (should-require-auth) {
|
if not (should-require-auth) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Fallback to configuration-based checks
|
|
||||||
let allow_skip = (config-get "security.bypass.allow_skip_auth" false)
|
let allow_skip = (config-get "security.bypass.allow_skip_auth" false)
|
||||||
require-auth $operation_name --allow-skip=$allow_skip
|
require-auth $operation_name --allow-skip=$allow_skip
|
||||||
|
|
||||||
# Get environment
|
|
||||||
let environment = (config-get "environment" "dev")
|
let environment = (config-get "environment" "dev")
|
||||||
|
|
||||||
# Check MFA requirements based on environment and operation type
|
|
||||||
if $environment == "prod" and (should-require-mfa-prod) {
|
if $environment == "prod" and (should-require-mfa-prod) {
|
||||||
require-mfa $operation_name "production environment"
|
require-mfa $operation_name "production environment"
|
||||||
} else if (is-destructive-operation $operation_type) and (should-require-mfa-destructive) {
|
} else if (is-destructive-operation $operation_type) and (should-require-mfa-destructive) {
|
||||||
|
|
@ -275,8 +331,8 @@ export def get-auth-metadata [] {
|
||||||
|
|
||||||
# Log authenticated operation for audit trail
|
# Log authenticated operation for audit trail
|
||||||
export def log-authenticated-operation [
|
export def log-authenticated-operation [
|
||||||
operation: string # Operation performed
|
operation: string
|
||||||
details: record # Operation details
|
details: record
|
||||||
] {
|
] {
|
||||||
let auth_metadata = (get-auth-metadata)
|
let auth_metadata = (get-auth-metadata)
|
||||||
|
|
||||||
|
|
@ -288,7 +344,6 @@ export def log-authenticated-operation [
|
||||||
mfa_verified: $auth_metadata.mfa_verified
|
mfa_verified: $auth_metadata.mfa_verified
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log to file if configured
|
|
||||||
let log_path = (config-get "security.audit_log_path" "")
|
let log_path = (config-get "security.audit_log_path" "")
|
||||||
if ($log_path | is-not-empty) {
|
if ($log_path | is-not-empty) {
|
||||||
let log_dir = ($log_path | path dirname)
|
let log_dir = ($log_path | path dirname)
|
||||||
|
|
@ -298,99 +353,79 @@ export def log-authenticated-operation [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Print current authentication status (user-friendly)
|
# Print current authentication status
|
||||||
export def print-auth-status [] {
|
export def print-auth-status [] {
|
||||||
let auth_status = (plugin-verify)
|
let auth_status = (plugin-verify)
|
||||||
let is_valid = ($auth_status | get valid? | default false)
|
let is_valid = ($auth_status | get valid? | default false)
|
||||||
|
|
||||||
print $"(ansi blue_bold)Authentication Status(ansi reset)"
|
print $"Authentication Status"
|
||||||
print $"━━━━━━━━━━━━━━━━━━━━━━━━"
|
print $"━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
if $is_valid {
|
if $is_valid {
|
||||||
let username = ($auth_status | get username? | default "unknown")
|
let username = ($auth_status | get username? | default "unknown")
|
||||||
let mfa_verified = ($auth_status | get mfa_verified? | default false)
|
let mfa_verified = ($auth_status | get mfa_verified? | default false)
|
||||||
|
|
||||||
print $"Status: (ansi green_bold)✓ Authenticated(ansi reset)"
|
print $"Status: ✓ Authenticated"
|
||||||
print $"User: (ansi cyan)($username)(ansi reset)"
|
print $"User: ($username)"
|
||||||
|
|
||||||
if $mfa_verified {
|
if $mfa_verified {
|
||||||
print $"MFA: (ansi green_bold)✓ Verified(ansi reset)"
|
print $"MFA: ✓ Verified"
|
||||||
} else {
|
} else {
|
||||||
print $"MFA: (ansi yellow)Not verified(ansi reset)"
|
print $"MFA: Not verified"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print $"Status: (ansi red)✗ Not authenticated(ansi reset)"
|
print $"Status: ✗ Not authenticated"
|
||||||
print ""
|
print ""
|
||||||
print $"Run: (ansi green)provisioning auth login <username>(ansi reset)"
|
print $"Run: provisioning auth login <username>"
|
||||||
}
|
}
|
||||||
|
|
||||||
print ""
|
print ""
|
||||||
print $"(ansi dim)Authentication required:(ansi reset) (should-require-auth)"
|
print $"Auth required: (should-require-auth)"
|
||||||
print $"(ansi dim)MFA for production:(ansi reset) (should-require-mfa-prod)"
|
print $"MFA for production: (should-require-mfa-prod)"
|
||||||
print $"(ansi dim)MFA for destructive:(ansi reset) (should-require-mfa-destructive)"
|
print $"MFA for destructive: (should-require-mfa-destructive)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# TYPEDIALOG HELPER FUNCTIONS
|
# TYPEDIALOG HELPER FUNCTIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Run TypeDialog form via bash wrapper for authentication
|
use ../utils/path-utils.nu *
|
||||||
# This pattern avoids TTY/input issues in Nushell's execution stack
|
|
||||||
|
# Run TypeDialog form and return parsed result
|
||||||
export def run-typedialog-auth-form [
|
export def run-typedialog-auth-form [
|
||||||
wrapper_script: string
|
form_path: string
|
||||||
--backend: string = "tui"
|
--backend: string = "tui"
|
||||||
] {
|
] {
|
||||||
# Check if the wrapper script exists
|
if (which typedialog | is-empty) {
|
||||||
if not ($wrapper_script | path exists) {
|
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: "TypeDialog wrapper not available"
|
error: "TypeDialog plugin not available"
|
||||||
use_fallback: true
|
use_fallback: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set backend environment variable
|
if not ($form_path | path exists) {
|
||||||
$env.TYPEDIALOG_BACKEND = $backend
|
|
||||||
|
|
||||||
# Run bash wrapper (handles TTY input properly)
|
|
||||||
let result = (do { bash $wrapper_script } | complete)
|
|
||||||
|
|
||||||
if $result.exit_code != 0 {
|
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: $result.stderr
|
error: $"Form not found: ($form_path)"
|
||||||
use_fallback: true
|
use_fallback: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read the generated JSON file
|
let result = (typedialog form $form_path --backend $backend)
|
||||||
let json_output = ($wrapper_script | path dirname | path join "generated" | path join ($wrapper_script | path basename | str replace ".sh" "-result.json"))
|
|
||||||
|
|
||||||
if not ($json_output | path exists) {
|
if ($result | is-empty) {
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: "Output file not found"
|
error: "Form cancelled by user"
|
||||||
use_fallback: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parse JSON output
|
|
||||||
let result = do {
|
|
||||||
open $json_output | from json
|
|
||||||
} | complete
|
|
||||||
|
|
||||||
if $result.exit_code == 0 {
|
|
||||||
let values = $result.stdout
|
|
||||||
{
|
|
||||||
success: true
|
|
||||||
values: $values
|
|
||||||
use_fallback: false
|
use_fallback: false
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
return {
|
|
||||||
success: false
|
{
|
||||||
error: "Failed to parse TypeDialog output"
|
success: true
|
||||||
use_fallback: true
|
values: $result
|
||||||
}
|
use_fallback: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,18 +433,16 @@ export def run-typedialog-auth-form [
|
||||||
# INTERACTIVE FORM HANDLERS (TypeDialog Integration)
|
# INTERACTIVE FORM HANDLERS (TypeDialog Integration)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Interactive login with form
|
# Interactive login with TypeDialog form
|
||||||
export def login-interactive [
|
export def login-interactive [
|
||||||
--backend: string = "tui"
|
--backend: string = "tui"
|
||||||
] : nothing -> record {
|
] : nothing -> record {
|
||||||
print "🔐 Interactive Authentication"
|
print "🔐 Interactive Authentication"
|
||||||
print ""
|
print ""
|
||||||
|
|
||||||
# Run the login form via bash wrapper
|
let form_path = (get-typedialog-form-path "auth-login.toml")
|
||||||
let wrapper_script = "provisioning/core/shlib/auth-login-tty.sh"
|
let form_result = (run-typedialog-auth-form $form_path --backend $backend)
|
||||||
let form_result = (run-typedialog-auth-form $wrapper_script --backend $backend)
|
|
||||||
|
|
||||||
# Fallback to basic prompts if TypeDialog not available
|
|
||||||
if not $form_result.success or $form_result.use_fallback {
|
if not $form_result.success or $form_result.use_fallback {
|
||||||
print "ℹ️ TypeDialog not available. Using basic prompts..."
|
print "ℹ️ TypeDialog not available. Using basic prompts..."
|
||||||
print ""
|
print ""
|
||||||
|
|
@ -449,7 +482,6 @@ export def login-interactive [
|
||||||
|
|
||||||
let form_values = $form_result.values
|
let form_values = $form_result.values
|
||||||
|
|
||||||
# Check if user cancelled or didn't confirm
|
|
||||||
if not ($form_values.auth?.confirm_login? | default false) {
|
if not ($form_values.auth?.confirm_login? | default false) {
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
|
|
@ -457,7 +489,6 @@ export def login-interactive [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Perform login with provided credentials
|
|
||||||
let username = ($form_values.auth?.username? | default "")
|
let username = ($form_values.auth?.username? | default "")
|
||||||
let password = ($form_values.auth?.password? | default "")
|
let password = ($form_values.auth?.password? | default "")
|
||||||
let has_mfa = ($form_values.auth?.has_mfa? | default false)
|
let has_mfa = ($form_values.auth?.has_mfa? | default false)
|
||||||
|
|
@ -474,7 +505,6 @@ export def login-interactive [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Call the plugin login function
|
|
||||||
let login_result = (plugin-login $username $password --mfa-code $mfa_code)
|
let login_result = (plugin-login $username $password --mfa-code $mfa_code)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -485,14 +515,13 @@ export def login-interactive [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Interactive MFA enrollment with form
|
# Interactive MFA enrollment with TypeDialog form
|
||||||
export def mfa-enroll-interactive [
|
export def mfa-enroll-interactive [
|
||||||
--backend: string = "tui"
|
--backend: string = "tui"
|
||||||
] : nothing -> record {
|
] : nothing -> record {
|
||||||
print "🔐 Multi-Factor Authentication Setup"
|
print "🔐 Multi-Factor Authentication Setup"
|
||||||
print ""
|
print ""
|
||||||
|
|
||||||
# Check if user is already authenticated
|
|
||||||
let auth_status = (plugin-verify)
|
let auth_status = (plugin-verify)
|
||||||
let is_authenticated = ($auth_status.valid // false)
|
let is_authenticated = ($auth_status.valid // false)
|
||||||
|
|
||||||
|
|
@ -503,11 +532,9 @@ export def mfa-enroll-interactive [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run the MFA enrollment form via bash wrapper
|
let form_path = (get-typedialog-form-path "mfa-enroll.toml")
|
||||||
let wrapper_script = "provisioning/core/shlib/mfa-enroll-tty.sh"
|
let form_result = (run-typedialog-auth-form $form_path --backend $backend)
|
||||||
let form_result = (run-typedialog-auth-form $wrapper_script --backend $backend)
|
|
||||||
|
|
||||||
# Fallback to basic prompts if TypeDialog not available
|
|
||||||
if not $form_result.success or $form_result.use_fallback {
|
if not $form_result.success or $form_result.use_fallback {
|
||||||
print "ℹ️ TypeDialog not available. Using basic prompts..."
|
print "ℹ️ TypeDialog not available. Using basic prompts..."
|
||||||
print ""
|
print ""
|
||||||
|
|
@ -518,52 +545,35 @@ export def mfa-enroll-interactive [
|
||||||
let device_name = if ($mfa_type == "totp" or $mfa_type == "webauthn") {
|
let device_name = if ($mfa_type == "totp" or $mfa_type == "webauthn") {
|
||||||
print "Device name: "
|
print "Device name: "
|
||||||
input
|
input
|
||||||
} else if $mfa_type == "sms" {
|
|
||||||
""
|
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
let phone_number = if $mfa_type == "sms" {
|
let phone_number = if $mfa_type == "sms" {
|
||||||
print "Phone number (international format, e.g., +1234567890): "
|
print "Phone number: "
|
||||||
input
|
input
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
let verification_code = if ($mfa_type == "totp" or $mfa_type == "sms") {
|
let verification_code = if ($mfa_type == "totp" or $mfa_type == "sms") {
|
||||||
print "Verification code (6 digits): "
|
print "Verification code: "
|
||||||
input
|
input
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
print "Generate backup codes? (y/n): "
|
|
||||||
let generate_backup_input = (input)
|
|
||||||
let generate_backup = ($generate_backup_input == "y" or $generate_backup_input == "Y")
|
|
||||||
|
|
||||||
let backup_count = if $generate_backup {
|
|
||||||
print "Number of backup codes (5-20): "
|
|
||||||
let count_str = (input)
|
|
||||||
$count_str | into int | default 10
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true
|
success: true
|
||||||
mfa_type: $mfa_type
|
mfa_type: $mfa_type
|
||||||
device_name: $device_name
|
device_name: $device_name
|
||||||
phone_number: $phone_number
|
phone_number: $phone_number
|
||||||
verification_code: $verification_code
|
verification_code: $verification_code
|
||||||
generate_backup_codes: $generate_backup
|
|
||||||
backup_codes_count: $backup_count
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let form_values = $form_result.values
|
let form_values = $form_result.values
|
||||||
|
|
||||||
# Check if user confirmed
|
|
||||||
if not ($form_values.mfa?.confirm_enroll? | default false) {
|
if not ($form_values.mfa?.confirm_enroll? | default false) {
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
|
|
@ -571,14 +581,11 @@ export def mfa-enroll-interactive [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract MFA type and parameters from form values
|
|
||||||
let mfa_type = ($form_values.mfa?.type? | default "totp")
|
let mfa_type = ($form_values.mfa?.type? | default "totp")
|
||||||
let device_name = if $mfa_type == "totp" {
|
let device_name = if $mfa_type == "totp" {
|
||||||
$form_values.mfa?.totp?.device_name? | default "Authenticator App"
|
$form_values.mfa?.totp?.device_name? | default "Authenticator App"
|
||||||
} else if $mfa_type == "webauthn" {
|
} else if $mfa_type == "webauthn" {
|
||||||
$form_values.mfa?.webauthn?.device_name? | default "Security Key"
|
$form_values.mfa?.webauthn?.device_name? | default "Security Key"
|
||||||
} else if $mfa_type == "sms" {
|
|
||||||
""
|
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -600,7 +607,6 @@ export def mfa-enroll-interactive [
|
||||||
let generate_backup = ($form_values.mfa?.generate_backup_codes? | default true)
|
let generate_backup = ($form_values.mfa?.generate_backup_codes? | default true)
|
||||||
let backup_count = ($form_values.mfa?.backup_codes_count? | default 10)
|
let backup_count = ($form_values.mfa?.backup_codes_count? | default 10)
|
||||||
|
|
||||||
# Call the plugin MFA enrollment function
|
|
||||||
let enroll_result = (plugin-mfa-enroll --type $mfa_type)
|
let enroll_result = (plugin-mfa-enroll --type $mfa_type)
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -614,3 +620,80 @@ export def mfa-enroll-interactive [
|
||||||
backup_codes_count: $backup_count
|
backup_codes_count: $backup_count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SIMPLE INPUT PROMPTS (for pipe and continue flows)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Get API key from user input - outputs to stdout for piping
|
||||||
|
export def get-api-key-interactive [] : nothing -> string {
|
||||||
|
print -n "Enter API Key: "
|
||||||
|
let api_key = (input --suppress-output)
|
||||||
|
|
||||||
|
if ($api_key | is-empty) {
|
||||||
|
print "Error: API key cannot be empty" | error
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get provider credentials - outputs JSON for continue flow
|
||||||
|
export def get-provider-credentials-interactive [] : nothing -> record {
|
||||||
|
print -n "Enter username: "
|
||||||
|
let username = (input)
|
||||||
|
|
||||||
|
print -n "Enter password: "
|
||||||
|
let password = (input --suppress-output)
|
||||||
|
print ""
|
||||||
|
|
||||||
|
if ($username | is-empty) or ($password | is-empty) {
|
||||||
|
print "Error: Username and password cannot be empty" | error
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
timestamp: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get secret configuration input - outputs JSON for continue flow
|
||||||
|
export def get-secret-config-interactive [] : nothing -> record {
|
||||||
|
print ""
|
||||||
|
print "═══════════════════════════════════════════════════════════════"
|
||||||
|
print "Secret Configuration"
|
||||||
|
print "═══════════════════════════════════════════════════════════════"
|
||||||
|
print ""
|
||||||
|
|
||||||
|
print "Choose secret backend:"
|
||||||
|
print " 1) SOPS (age/gpg encryption)"
|
||||||
|
print " 2) HashiCorp Vault"
|
||||||
|
print " 3) AWS Secrets Manager"
|
||||||
|
print ""
|
||||||
|
print -n "Select backend (1-3): "
|
||||||
|
let backend_choice = (input)
|
||||||
|
|
||||||
|
let backend = match $backend_choice {
|
||||||
|
"1" => "sops"
|
||||||
|
"2" => "vault"
|
||||||
|
"3" => "aws-secrets"
|
||||||
|
_ => "sops"
|
||||||
|
}
|
||||||
|
|
||||||
|
print ""
|
||||||
|
print -n "Enter secret location/path: "
|
||||||
|
let secret_path = (input)
|
||||||
|
|
||||||
|
if ($secret_path | is-empty) {
|
||||||
|
print "Error: Secret path cannot be empty" | error
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
backend: $backend
|
||||||
|
secret_path: $secret_path
|
||||||
|
timestamp: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
|
||||||
# Check if KMS plugin is available
|
# Check if KMS plugin is available (registered with Nushell)
|
||||||
def is-plugin-available [] {
|
def is-plugin-available [] {
|
||||||
(which kms | length) > 0
|
let installed = (version | get installed_plugins)
|
||||||
|
$installed | str contains "kms"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if KMS plugin is enabled in config
|
# Check if KMS plugin is enabled in config
|
||||||
|
|
@ -62,7 +63,7 @@ export def plugin-kms-encrypt [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback - call KMS service directly
|
# HTTP fallback - call KMS service directly
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/encrypt"
|
let url = $"($kms_url)/api/encrypt"
|
||||||
|
|
@ -119,7 +120,7 @@ export def plugin-kms-decrypt [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback - call KMS service directly
|
# HTTP fallback - call KMS service directly
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/decrypt"
|
let url = $"($kms_url)/api/decrypt"
|
||||||
|
|
@ -171,7 +172,7 @@ export def plugin-kms-generate-key [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/keys/generate"
|
let url = $"($kms_url)/api/keys/generate"
|
||||||
|
|
@ -216,7 +217,7 @@ export def plugin-kms-status [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/health"
|
let url = $"($kms_url)/health"
|
||||||
|
|
@ -253,7 +254,7 @@ export def plugin-kms-backends [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/backends"
|
let url = $"($kms_url)/api/backends"
|
||||||
|
|
@ -299,7 +300,7 @@ export def plugin-kms-rotate-key [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/keys/rotate"
|
let url = $"($kms_url)/api/keys/rotate"
|
||||||
|
|
@ -342,7 +343,7 @@ export def plugin-kms-list-keys [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_kms not available - using HTTP fallback for key management"
|
||||||
|
|
||||||
let kms_url = (get-kms-url)
|
let kms_url = (get-kms-url)
|
||||||
let url = $"($kms_url)/api/keys?backend=($backend_name)"
|
let url = $"($kms_url)/api/keys?backend=($backend_name)"
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
|
||||||
# Check if orchestrator plugin is available
|
# Check if orchestrator plugin is available (registered with Nushell)
|
||||||
def is-plugin-available [] {
|
def is-plugin-available [] {
|
||||||
(which orch | length) > 0
|
let installed = (version | get installed_plugins)
|
||||||
|
$installed | str contains "orchestrator"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if orchestrator plugin is enabled in config
|
# Check if orchestrator plugin is enabled in config
|
||||||
|
|
@ -15,7 +16,11 @@ def is-plugin-enabled [] {
|
||||||
|
|
||||||
# Get orchestrator base URL
|
# Get orchestrator base URL
|
||||||
def get-orchestrator-url [] {
|
def get-orchestrator-url [] {
|
||||||
config-get "platform.orchestrator.url" "http://localhost:8080"
|
if ($env.PROVISIONING_ORCHESTRATOR_URL? | is-not-empty) {
|
||||||
|
$env.PROVISIONING_ORCHESTRATOR_URL
|
||||||
|
} else {
|
||||||
|
config-get "platform.orchestrator.url" "http://localhost:9011"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get orchestrator data directory
|
# Get orchestrator data directory
|
||||||
|
|
@ -68,7 +73,7 @@ export def plugin-orch-status [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_orchestrator not available - using HTTP fallback for orchestration"
|
||||||
|
|
||||||
let url = $"(get-orchestrator-url)/health"
|
let url = $"(get-orchestrator-url)/health"
|
||||||
|
|
||||||
|
|
@ -150,7 +155,7 @@ export def plugin-orch-tasks [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_orchestrator not available - using HTTP fallback for orchestration"
|
||||||
|
|
||||||
let orch_url = get-orchestrator-url
|
let orch_url = get-orchestrator-url
|
||||||
let url = if ($status | is-empty) {
|
let url = if ($status | is-empty) {
|
||||||
|
|
@ -212,7 +217,7 @@ export def plugin-orch-task [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_orchestrator not available - using HTTP fallback for orchestration"
|
||||||
|
|
||||||
let orch_url = get-orchestrator-url
|
let orch_url = get-orchestrator-url
|
||||||
let url = $"($orch_url)/tasks/($task_id)"
|
let url = $"($orch_url)/tasks/($task_id)"
|
||||||
|
|
@ -248,7 +253,7 @@ export def plugin-orch-validate [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_orchestrator not available - using HTTP fallback for orchestration"
|
||||||
|
|
||||||
let orch_url = get-orchestrator-url
|
let orch_url = get-orchestrator-url
|
||||||
let url = $"($orch_url)/validate"
|
let url = $"($orch_url)/validate"
|
||||||
|
|
@ -329,7 +334,7 @@ export def plugin-orch-stats [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_orchestrator not available - using HTTP fallback for orchestration"
|
||||||
|
|
||||||
let orch_url = get-orchestrator-url
|
let orch_url = get-orchestrator-url
|
||||||
let url = $"($orch_url)/stats"
|
let url = $"($orch_url)/stats"
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
|
||||||
# Check if SecretumVault plugin is available
|
# Check if SecretumVault plugin is available (registered with Nushell)
|
||||||
def is-plugin-available [] {
|
def is-plugin-available [] {
|
||||||
(which secretumvault | length) > 0
|
let installed = (version | get installed_plugins)
|
||||||
|
$installed | str contains "secretumvault"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if SecretumVault plugin is enabled in config
|
# Check if SecretumVault plugin is enabled in config
|
||||||
|
|
@ -77,7 +78,7 @@ export def plugin-secretumvault-encrypt [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback - call SecretumVault service directly
|
# HTTP fallback - call SecretumVault service directly
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let sv_token = (get-secretumvault-token)
|
let sv_token = (get-secretumvault-token)
|
||||||
|
|
@ -142,7 +143,7 @@ export def plugin-secretumvault-decrypt [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback - call SecretumVault service directly
|
# HTTP fallback - call SecretumVault service directly
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let sv_token = (get-secretumvault-token)
|
let sv_token = (get-secretumvault-token)
|
||||||
|
|
@ -215,7 +216,7 @@ export def plugin-secretumvault-generate-key [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let sv_token = (get-secretumvault-token)
|
let sv_token = (get-secretumvault-token)
|
||||||
|
|
@ -266,7 +267,7 @@ export def plugin-secretumvault-health [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let url = $"($sv_url)/v1/sys/health"
|
let url = $"($sv_url)/v1/sys/health"
|
||||||
|
|
@ -304,7 +305,7 @@ export def plugin-secretumvault-version [] {
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let url = $"($sv_url)/v1/sys/health"
|
let url = $"($sv_url)/v1/sys/health"
|
||||||
|
|
@ -348,7 +349,7 @@ export def plugin-secretumvault-rotate-key [
|
||||||
}
|
}
|
||||||
|
|
||||||
# HTTP fallback
|
# HTTP fallback
|
||||||
print "⚠️ Using HTTP fallback (plugin not available)"
|
print "⚠️ nu_plugin_secretumvault not available - using HTTP fallback for secrets"
|
||||||
|
|
||||||
let sv_url = (get-secretumvault-url)
|
let sv_url = (get-secretumvault-url)
|
||||||
let sv_token = (get-secretumvault-token)
|
let sv_token = (get-secretumvault-token)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use utils *
|
use utils *
|
||||||
use config/accessor.nu *
|
use config/accessor.nu *
|
||||||
|
use ./utils/nickel_processor.nu [ncl-eval]
|
||||||
|
|
||||||
export def clip_copy [
|
export def clip_copy [
|
||||||
msg: string
|
msg: string
|
||||||
|
|
@ -90,11 +91,18 @@ export def process_decl_file [
|
||||||
] {
|
] {
|
||||||
# Use external Nickel CLI (nickel export)
|
# Use external Nickel CLI (nickel export)
|
||||||
if (get-use-nickel) {
|
if (get-use-nickel) {
|
||||||
let result = (^nickel export $decl_file --format $format | complete)
|
# Note: format parameter is only used if it's "json"; otherwise raw nickel export is needed
|
||||||
if $result.exit_code == 0 {
|
if $format == "json" {
|
||||||
$result.stdout
|
let result = (ncl-eval $decl_file [])
|
||||||
|
$result | to json
|
||||||
} else {
|
} else {
|
||||||
error make { msg: $result.stderr }
|
# For non-JSON formats, use raw nickel command
|
||||||
|
let result = (do { ^nickel export $decl_file --format $format } | complete)
|
||||||
|
if $result.exit_code == 0 {
|
||||||
|
$result.stdout
|
||||||
|
} else {
|
||||||
|
error make { msg: $result.stderr }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error make { msg: "Nickel CLI not available" }
|
error make { msg: "Nickel CLI not available" }
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export def detect-project [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = [
|
mut args = [
|
||||||
"detect"
|
"detect"
|
||||||
$project_path
|
$project_path
|
||||||
"--format" $format
|
"--format" $format
|
||||||
|
|
@ -68,7 +68,7 @@ export def complete-project [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = [
|
mut args = [
|
||||||
"complete"
|
"complete"
|
||||||
$project_path
|
$project_path
|
||||||
"--format" $format
|
"--format" $format
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,25 @@ use registry.nu *
|
||||||
use interface.nu *
|
use interface.nu *
|
||||||
use ../utils/logging.nu *
|
use ../utils/logging.nu *
|
||||||
|
|
||||||
# Load provider dynamically with validation
|
# Load provider dynamically with validation (cached)
|
||||||
export def load-provider [name: string] {
|
export def load-provider [name: string] {
|
||||||
# Silent loading - only log errors, not info/success
|
# Check cache first - provider loading happens multiple times due to wrapper scripts
|
||||||
# Provider loading happens multiple times due to wrapper scripts, logging creates noise
|
let cache_key = $"PROVIDER_CACHE_($name)"
|
||||||
|
if ($cache_key in ($env | columns)) {
|
||||||
|
return ($env | get $cache_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Silent loading - only log debug, not errors for repeated loads
|
||||||
|
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||||
|
log-debug $"Loading provider: ($name)" "provider-loader"
|
||||||
|
}
|
||||||
|
|
||||||
# Check if provider is available
|
# Check if provider is available
|
||||||
if not (is-provider-available $name) {
|
if not (is-provider-available $name) {
|
||||||
log-error $"Provider ($name) not found or not available" "provider-loader"
|
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||||
|
log-debug $"Provider ($name) not found or not available" "provider-loader"
|
||||||
|
}
|
||||||
|
load-env { $cache_key: {} }
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,17 +38,33 @@ export def load-provider [name: string] {
|
||||||
}
|
}
|
||||||
|
|
||||||
if not ($provider_instance | is-empty) {
|
if not ($provider_instance | is-empty) {
|
||||||
# Validate interface compliance
|
# IMPORTANT: Skip subprocess-based validation for extension providers.
|
||||||
let validation = (validate-provider-interface $name $provider_instance)
|
# Child nu processes don't inherit NICKEL_IMPORT_PATH or the provisioning env,
|
||||||
|
# so validate-provider-interface always reports functions missing even when valid.
|
||||||
|
# (Same documented fix as registry.nu:132-146 and load-extension-provider above)
|
||||||
|
# Core providers are loaded from known paths where subprocess context is reliable.
|
||||||
|
let skip_validation = ($provider_entry.type == "extension")
|
||||||
|
let validation = if $skip_validation {
|
||||||
|
{ valid: true, missing_functions: [] }
|
||||||
|
} else {
|
||||||
|
validate-provider-interface $name $provider_instance
|
||||||
|
}
|
||||||
if $validation.valid {
|
if $validation.valid {
|
||||||
|
load-env { $cache_key: $provider_instance }
|
||||||
$provider_instance
|
$provider_instance
|
||||||
} else {
|
} else {
|
||||||
log-error $"Provider ($name) failed interface validation" "provider-loader"
|
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||||
log-error $"Missing functions: ($validation.missing_functions | str join ', ')" "provider-loader"
|
log-error $"Provider ($name) failed interface validation" "provider-loader"
|
||||||
|
log-error $"Missing functions: ($validation.missing_functions | str join ', ')" "provider-loader"
|
||||||
|
}
|
||||||
|
load-env { $cache_key: {} }
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log-error $"Failed to load provider module for ($name)" "provider-loader"
|
if ($env.PROVISIONING_DEBUG? | default false) {
|
||||||
|
log-error $"Failed to load provider module for ($name)" "provider-loader"
|
||||||
|
}
|
||||||
|
load-env { $cache_key: {} }
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,26 +87,25 @@ def load-core-provider [provider_entry: record] {
|
||||||
|
|
||||||
# Load extension provider
|
# Load extension provider
|
||||||
def load-extension-provider [provider_entry: record] {
|
def load-extension-provider [provider_entry: record] {
|
||||||
# For extension providers, use the adapter pattern
|
# IMPORTANT: Do NOT spawn a child nu process to validate the provider.
|
||||||
|
# Child processes don't inherit NICKEL_IMPORT_PATH or the provisioning env,
|
||||||
|
# causing all providers to fail validation even though they are valid.
|
||||||
|
# (Same reason registry.nu skips subprocess validation — see registry.nu:132-146)
|
||||||
|
# Just verify the file exists and create the instance directly.
|
||||||
let module_path = $provider_entry.entry_point
|
let module_path = $provider_entry.entry_point
|
||||||
|
|
||||||
# Test that the provider file exists and has the required functions
|
if not ($module_path | path exists) {
|
||||||
let test_cmd = $"nu -c \"use ($module_path) *; get-provider-metadata | to json\""
|
log-error $"Provider module not found: ($module_path)" "provider-loader"
|
||||||
let test_result_check = (do { nu -c $test_cmd | complete })
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
if ($test_result_check.exit_code != 0) {
|
{
|
||||||
log-error $"Provider validation failed for ($provider_entry.name)" "provider-loader"
|
name: $provider_entry.name
|
||||||
{}
|
type: "extension"
|
||||||
} else {
|
loaded: true
|
||||||
# Create provider instance record
|
entry_point: $module_path
|
||||||
{
|
load_time: (date now)
|
||||||
name: $provider_entry.name
|
metadata: {}
|
||||||
type: "extension"
|
|
||||||
loaded: true
|
|
||||||
entry_point: $module_path
|
|
||||||
load_time: (date now)
|
|
||||||
metadata: ($test_result_check.stdout | from json)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +177,7 @@ let args = \(open ($args_file)\)
|
||||||
$script_content | save --force $wrapper_script
|
$script_content | save --force $wrapper_script
|
||||||
|
|
||||||
# Execute the wrapper script
|
# Execute the wrapper script
|
||||||
let result = (do { nu $wrapper_script } | complete)
|
let result = (do --ignore-errors { nu $wrapper_script } | complete)
|
||||||
|
|
||||||
# Clean up temp files
|
# Clean up temp files
|
||||||
if ($args_file | path exists) { rm -f $args_file }
|
if ($args_file | path exists) { rm -f $args_file }
|
||||||
|
|
@ -159,24 +185,17 @@ let args = \(open ($args_file)\)
|
||||||
|
|
||||||
# Return result if successful, null otherwise
|
# Return result if successful, null otherwise
|
||||||
if $result.exit_code == 0 {
|
if $result.exit_code == 0 {
|
||||||
# Try to parse as structured data (JSON, NUON, etc), fallback to string
|
# Parse output: always try JSON first (handles strings, bools, records, lists)
|
||||||
|
# The wrapper script serializes all return values with | to json, so bare JSON
|
||||||
|
# strings like "91.98.28.202" must go through from json to strip the quotes.
|
||||||
let output = ($result.stdout | str trim)
|
let output = ($result.stdout | str trim)
|
||||||
if ($output | is-empty) {
|
if ($output | is-empty) {
|
||||||
null
|
null
|
||||||
} else if $output == "true" {
|
|
||||||
true
|
|
||||||
} else if $output == "false" {
|
|
||||||
false
|
|
||||||
} else if ($output | str starts-with "{") or ($output | str starts-with "[") {
|
|
||||||
# Try JSON parse - use error handling for Nushell 0.107
|
|
||||||
let parsed = (do -i { $output | from json })
|
|
||||||
if ($parsed | is-empty) {
|
|
||||||
$output
|
|
||||||
} else {
|
|
||||||
$parsed
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$output
|
let parsed = (do -i { $output | from json })
|
||||||
|
let value = if ($parsed | is-empty) { $output } else { $parsed }
|
||||||
|
log-debug $"($provider_name)::($function_name) → ($value)" "provider-loader"
|
||||||
|
$value
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log-error $"Provider function call failed: ($result.stderr)" "provider-loader"
|
log-error $"Provider function call failed: ($result.stderr)" "provider-loader"
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ def discover-providers-only [] {
|
||||||
mut registry = {}
|
mut registry = {}
|
||||||
|
|
||||||
# Get provisioning system path from config or environment
|
# Get provisioning system path from config or environment
|
||||||
let base_path = (config-get "provisioning.path" ($env.PROVISIONING? | default "/Users/Akasha/project-provisioning/provisioning"))
|
let base_path = (config-get "provisioning.path" ($env.PROVISIONING? | default ($env.HOME | path join "project-provisioning/provisioning")))
|
||||||
|
|
||||||
# PRIORITY 1: Workspace .providers (if in workspace context)
|
# PRIORITY 1: Workspace .providers (if in workspace context)
|
||||||
# Look for .providers in workspace root or parent directories
|
# Look for .providers in workspace root or parent directories
|
||||||
|
|
@ -129,31 +129,33 @@ def discover-providers-in-directory [base_path: string, provider_type: string] {
|
||||||
if ($provider_file | path exists) {
|
if ($provider_file | path exists) {
|
||||||
let provider_name = ($dir | path basename)
|
let provider_name = ($dir | path basename)
|
||||||
|
|
||||||
|
# COMMENTED OUT: Metadata verification was causing silent failures
|
||||||
|
# The nu -c subprocess doesn't have proper NICKEL_IMPORT_PATH configured
|
||||||
|
# This caused all providers to be skipped even though they are valid
|
||||||
|
# Solution: Just mark all providers with provider.nu as available
|
||||||
|
# Actual metadata loading happens when the provider is used
|
||||||
|
|
||||||
# Check if provider has metadata function (just test it's valid)
|
# Check if provider has metadata function (just test it's valid)
|
||||||
# We don't parse the metadata here, just verify the provider loads
|
# We don't parse the metadata here, just verify the provider loads
|
||||||
# Suppress error output by redirecting to /dev/null
|
# Suppress error output by redirecting to /dev/null
|
||||||
let has_metadata = (do {
|
# let has_metadata = (do {
|
||||||
^nu -c $"use ($provider_file) *; get-provider-metadata | ignore" o> /dev/null e> /dev/null
|
# ^nu -c $"use ($provider_file) *; get-provider-metadata | ignore" o> /dev/null e> /dev/null
|
||||||
} | complete | get exit_code) == 0
|
# } | complete | get exit_code) == 0
|
||||||
|
|
||||||
if $has_metadata {
|
# if $has_metadata { ... } else { ... }
|
||||||
let provider_info = {
|
# INSTEAD: Simply register any provider.nu file as available
|
||||||
name: $provider_name
|
let provider_info = {
|
||||||
type: $provider_type
|
name: $provider_name
|
||||||
path: $dir
|
type: $provider_type
|
||||||
entry_point: $provider_file
|
path: $dir
|
||||||
available: true
|
entry_point: $provider_file
|
||||||
loaded: false
|
available: true
|
||||||
last_discovered: (date now)
|
loaded: false
|
||||||
}
|
last_discovered: (date now)
|
||||||
|
|
||||||
$providers = ($providers | insert $provider_name $provider_info)
|
|
||||||
log-debug $" 📦 Found ($provider_type) provider: ($provider_name)" "provider-discovery"
|
|
||||||
} else {
|
|
||||||
# Silently skip invalid providers instead of warning
|
|
||||||
# This can happen with providers that have bugs - they'll be marked as unavailable
|
|
||||||
log-debug $" ⊘ Skipping invalid provider: ($provider_name)" "provider-discovery"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$providers = ($providers | insert $provider_name $provider_info)
|
||||||
|
log-debug $" 📦 Found ($provider_type) provider: ($provider_name)" "provider-discovery"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export def combine [result1: record, result2: record] {
|
||||||
# Combine list of Results (stops on first error)
|
# Combine list of Results (stops on first error)
|
||||||
# Type: list -> record
|
# Type: list -> record
|
||||||
export def combine-all [results: list] {
|
export def combine-all [results: list] {
|
||||||
let mut accumulated = (ok [])
|
mut accumulated = (ok [])
|
||||||
|
|
||||||
for result in $results {
|
for result in $results {
|
||||||
if (is-err $accumulated) {
|
if (is-err $accumulated) {
|
||||||
|
|
@ -133,11 +133,11 @@ export def try-wrap [fn: closure] {
|
||||||
|
|
||||||
# Match on Result (like Rust's match)
|
# Match on Result (like Rust's match)
|
||||||
# Type: record, closure, closure -> any
|
# Type: record, closure, closure -> any
|
||||||
export def match-result [result: record, on-ok: closure, on-err: closure] {
|
export def match-result [result: record, on_ok: closure, on_err: closure] {
|
||||||
if (is-ok $result) {
|
if (is-ok $result) {
|
||||||
do $on-ok $result.ok
|
do $on_ok $result.ok
|
||||||
} else {
|
} else {
|
||||||
do $on-err $result.err
|
do $on_err $result.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,32 +49,20 @@ def http-health-check [
|
||||||
config: record
|
config: record
|
||||||
] {
|
] {
|
||||||
let timeout = $config.timeout? | default 5
|
let timeout = $config.timeout? | default 5
|
||||||
|
let expected_status = ($config.expected_status? | default 200)
|
||||||
|
let timeout_dur = ($"($timeout)sec" | into duration)
|
||||||
|
|
||||||
let http_result = (do {
|
let response = (try {
|
||||||
http get --max-time ($timeout | into string + "s") $config.endpoint
|
http head --allow-errors --full --max-time $timeout_dur $config.endpoint
|
||||||
} | complete)
|
} catch {
|
||||||
|
return { healthy: false, message: "HTTP health check failed - endpoint unreachable" }
|
||||||
|
})
|
||||||
|
|
||||||
if $http_result.exit_code == 0 {
|
let status = $response.status
|
||||||
# For simple health endpoints that return strings
|
if $status == $expected_status {
|
||||||
{ healthy: true, message: "HTTP health check passed" }
|
{ healthy: true, message: $"HTTP status ($status) matches expected" }
|
||||||
} else {
|
} else {
|
||||||
# Try with curl for more control
|
{ healthy: false, message: $"HTTP status ($status) != expected ($expected_status)" }
|
||||||
let curl_result = (do {
|
|
||||||
curl -s -o /dev/null -w "%{http_code}" -m $timeout $config.endpoint
|
|
||||||
} | complete)
|
|
||||||
|
|
||||||
if $curl_result.exit_code == 0 {
|
|
||||||
let status_code = $curl_result.stdout
|
|
||||||
let expected = ($config.expected_status | into string)
|
|
||||||
|
|
||||||
if $status_code == $expected {
|
|
||||||
{ healthy: true, message: $"HTTP status [$status_code] matches expected" }
|
|
||||||
} else {
|
|
||||||
{ healthy: false, message: $"HTTP status [$status_code] != expected [$expected]" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{ healthy: false, message: "HTTP health check failed - endpoint unreachable" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,8 +140,7 @@ export def retry-health-check [
|
||||||
|
|
||||||
if $attempt < ($max_retries + 1) {
|
if $attempt < ($max_retries + 1) {
|
||||||
print $"Health check failed (attempt ($attempt)/($max_retries)), retrying in ($interval)s..."
|
print $"Health check failed (attempt ($attempt)/($max_retries)), retrying in ($interval)s..."
|
||||||
let interval_str = $interval | into string
|
sleep ($"($interval)sec" | into duration)
|
||||||
sleep ($"($interval_str)sec" | into duration)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,8 +185,7 @@ export def wait-for-service [
|
||||||
}
|
}
|
||||||
|
|
||||||
print $"Waiting for ($service)... (($check_result.message))"
|
print $"Waiting for ($service)... (($check_result.message))"
|
||||||
let sleep_duration = ($interval | into string) + "sec"
|
sleep ($"($interval)sec" | into duration)
|
||||||
sleep ($sleep_duration | into duration)
|
|
||||||
|
|
||||||
wait_loop $service $config $start $timeout_ns $interval
|
wait_loop $service $config $start $timeout_ns $interval
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +247,6 @@ export def monitor-service-health [
|
||||||
print $"⚠️ ALERT: Service ($service_name) is unhealthy!"
|
print $"⚠️ ALERT: Service ($service_name) is unhealthy!"
|
||||||
}
|
}
|
||||||
|
|
||||||
let sleep_duration = ($interval | into string) + "sec"
|
sleep ($"($interval)sec" | into duration)
|
||||||
sleep ($sleep_duration | into duration)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export def install_config [
|
||||||
let reset = ($ops | str contains "reset")
|
let reset = ($ops | str contains "reset")
|
||||||
let use_context = if ($ops | str contains "context") or $context { true } else { false }
|
let use_context = if ($ops | str contains "context") or $context { true } else { false }
|
||||||
let provisioning_config_path = $nu.default-config-dir | path dirname | path join $provisioning_cfg_name | path join "nushell"
|
let provisioning_config_path = $nu.default-config-dir | path dirname | path join $provisioning_cfg_name | path join "nushell"
|
||||||
let provisioning_root = if ((get-base-path) | is-not-empty) {
|
let provisioning_root = if ((get-config-base-path) | is-not-empty) {
|
||||||
(get-base-path)
|
(get-config-base-path)
|
||||||
} else {
|
} else {
|
||||||
let base_path = if ($env.PROCESS_PATH | str contains "provisioning") {
|
let base_path = if ($env.PROCESS_PATH | str contains "provisioning") {
|
||||||
$env.PROCESS_PATH
|
$env.PROCESS_PATH
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use ../utils/logging.nu *
|
||||||
# Re-export existing utilities and config helpers
|
# Re-export existing utilities and config helpers
|
||||||
export use utils.nu *
|
export use utils.nu *
|
||||||
export use config.nu *
|
export use config.nu *
|
||||||
|
# Note: wizard.nu is imported by callers directly - avoid circular import with mod.nu
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONFIGURATION PATH HELPERS
|
# CONFIGURATION PATH HELPERS
|
||||||
|
|
@ -34,7 +35,7 @@ export def get-config-base-path [] {
|
||||||
|
|
||||||
# Get provisioning installation path
|
# Get provisioning installation path
|
||||||
export def get-install-path [] {
|
export def get-install-path [] {
|
||||||
config-get "setup.install_path" (get-base-path)
|
config-get "setup.install_path" (get-config-base-path)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get global workspaces directory
|
# Get global workspaces directory
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use ./mod.nu *
|
||||||
use ./detection.nu *
|
use ./detection.nu *
|
||||||
use ./validation.nu *
|
use ./validation.nu *
|
||||||
use ./wizard.nu *
|
use ./wizard.nu *
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SYSTEM CONFIGURATION CREATION
|
# SYSTEM CONFIGURATION CREATION
|
||||||
|
|
@ -253,10 +254,10 @@ export def setup-cedar-policies [
|
||||||
# Get Nickel schema path for config type
|
# Get Nickel schema path for config type
|
||||||
def get-nickel-schema-path [config_type: string] {
|
def get-nickel-schema-path [config_type: string] {
|
||||||
match $config_type {
|
match $config_type {
|
||||||
"system" => "provisioning/schemas/platform/schemas/system.ncl"
|
"system" => "provisioning/schemas/platform/system.ncl"
|
||||||
"deployment" => "provisioning/schemas/platform/schemas/deployment.ncl"
|
"deployment" => "provisioning/schemas/platform/deployment.ncl"
|
||||||
"user_preferences" => "provisioning/schemas/platform/schemas/user_preferences.ncl"
|
"user_preferences" => "provisioning/schemas/platform/user_preferences.ncl"
|
||||||
"provider" => "provisioning/schemas/platform/schemas/provider.ncl"
|
"provider" => "provisioning/schemas/platform/provider.ncl"
|
||||||
_ => ""
|
_ => ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -279,7 +280,7 @@ export def create-system-config-nickel [
|
||||||
# Profile: ($profile)
|
# Profile: ($profile)
|
||||||
|
|
||||||
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
||||||
let system_schema = import \"../../schemas/platform/schemas/system.ncl\" in
|
let system_schema = import \"../../schemas/platform/system.ncl\" in
|
||||||
let defaults = import \"../../schemas/platform/defaults/system-defaults.ncl\" in
|
let defaults = import \"../../schemas/platform/defaults/system-defaults.ncl\" in
|
||||||
|
|
||||||
# Compose: defaults + platform-specific values
|
# Compose: defaults + platform-specific values
|
||||||
|
|
@ -324,7 +325,7 @@ export def create-platform-config-nickel [
|
||||||
# Deployment Mode: ($deployment_mode)
|
# Deployment Mode: ($deployment_mode)
|
||||||
|
|
||||||
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
||||||
let deployment_schema = import \"../../schemas/platform/schemas/deployment.ncl\" in
|
let deployment_schema = import \"../../schemas/platform/deployment.ncl\" in
|
||||||
let defaults = import \"../../schemas/platform/defaults/deployment-defaults.ncl\" in
|
let defaults = import \"../../schemas/platform/defaults/deployment-defaults.ncl\" in
|
||||||
|
|
||||||
# Profile-specific overlay
|
# Profile-specific overlay
|
||||||
|
|
@ -370,7 +371,7 @@ export def create-user-preferences-nickel [
|
||||||
# Profile: ($profile)
|
# Profile: ($profile)
|
||||||
|
|
||||||
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
let helpers = import \"../../schemas/platform/common/helpers.ncl\" in
|
||||||
let prefs_schema = import \"../../schemas/platform/schemas/user_preferences.ncl\" in
|
let prefs_schema = import \"../../schemas/platform/user_preferences.ncl\" in
|
||||||
let defaults = import \"../../schemas/platform/defaults/user_preferences-defaults.ncl\" in
|
let defaults = import \"../../schemas/platform/defaults/user_preferences-defaults.ncl\" in
|
||||||
|
|
||||||
# Profile-specific overlay (production has stricter defaults)
|
# Profile-specific overlay (production has stricter defaults)
|
||||||
|
|
@ -410,7 +411,7 @@ export def create-provider-config-nickel [
|
||||||
$"# UpCloud Provider Configuration (Nickel)
|
$"# UpCloud Provider Configuration (Nickel)
|
||||||
# Generated: (get-timestamp-iso8601)
|
# Generated: (get-timestamp-iso8601)
|
||||||
|
|
||||||
let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
let provider_schema = import \"../../schemas/platform/provider.ncl\" in
|
||||||
|
|
||||||
{
|
{
|
||||||
api_url = \"https://api.upcloud.com/1.3\",
|
api_url = \"https://api.upcloud.com/1.3\",
|
||||||
|
|
@ -425,7 +426,7 @@ let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
||||||
$"# AWS Provider Configuration (Nickel)
|
$"# AWS Provider Configuration (Nickel)
|
||||||
# Generated: (get-timestamp-iso8601)
|
# Generated: (get-timestamp-iso8601)
|
||||||
|
|
||||||
let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
let provider_schema = import \"../../schemas/platform/provider.ncl\" in
|
||||||
|
|
||||||
{
|
{
|
||||||
region = \"us-east-1\",
|
region = \"us-east-1\",
|
||||||
|
|
@ -439,7 +440,7 @@ let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
||||||
$"# Hetzner Provider Configuration (Nickel)
|
$"# Hetzner Provider Configuration (Nickel)
|
||||||
# Generated: (get-timestamp-iso8601)
|
# Generated: (get-timestamp-iso8601)
|
||||||
|
|
||||||
let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
let provider_schema = import \"../../schemas/platform/provider.ncl\" in
|
||||||
|
|
||||||
{
|
{
|
||||||
api_url = \"https://api.hetzner.cloud/v1\",
|
api_url = \"https://api.hetzner.cloud/v1\",
|
||||||
|
|
@ -453,7 +454,7 @@ let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
||||||
$"# Local Provider Configuration (Nickel)
|
$"# Local Provider Configuration (Nickel)
|
||||||
# Generated: (get-timestamp-iso8601)
|
# Generated: (get-timestamp-iso8601)
|
||||||
|
|
||||||
let provider_schema = import \"../../schemas/platform/schemas/provider.ncl\" in
|
let provider_schema = import \"../../schemas/platform/provider.ncl\" in
|
||||||
|
|
||||||
{
|
{
|
||||||
base_path = \"/tmp/provisioning-local\",
|
base_path = \"/tmp/provisioning-local\",
|
||||||
|
|
@ -538,7 +539,7 @@ export def export-nickel-to-toml [
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run nickel export
|
# Run nickel export
|
||||||
let export_result = (do { nickel export --format toml $ncl_path | save -f $toml_path } | complete)
|
let export_result = (do { ^nickel export --format toml $ncl_path | save -f $toml_path } | complete)
|
||||||
|
|
||||||
if ($export_result.exit_code == 0) {
|
if ($export_result.exit_code == 0) {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#use ../lib_provisioning/defs/lists.nu providers_list
|
#use ../lib_provisioning/defs/lists.nu providers_list
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
export def setup_config_path [
|
export def setup_config_path [
|
||||||
provisioning_cfg_name: string = "provisioning"
|
provisioning_cfg_name: string = "provisioning"
|
||||||
|
|
@ -11,7 +12,7 @@ export def tools_install [
|
||||||
run_args?: string
|
run_args?: string
|
||||||
] {
|
] {
|
||||||
print $"(_ansi cyan)((get-provisioning-name))(_ansi reset) (_ansi yellow_bold)tools(_ansi reset) check:\n"
|
print $"(_ansi cyan)((get-provisioning-name))(_ansi reset) (_ansi yellow_bold)tools(_ansi reset) check:\n"
|
||||||
let bin_install = ((get-base-path) | path join "core" | path join "bin" | path join "tools-install")
|
let bin_install = ((get-config-base-path) | path join "core" | path join "bin" | path join "tools-install")
|
||||||
if not ($bin_install | path exists) {
|
if not ($bin_install | path exists) {
|
||||||
print $"🛑 Error running (_ansi yellow)tools_install(_ansi reset) not found (_ansi red_bold)($bin_install | path basename)(_ansi reset)"
|
print $"🛑 Error running (_ansi yellow)tools_install(_ansi reset) not found (_ansi red_bold)($bin_install | path basename)(_ansi reset)"
|
||||||
if (is-debug-enabled) { print $"($bin_install)" }
|
if (is-debug-enabled) { print $"($bin_install)" }
|
||||||
|
|
@ -58,7 +59,7 @@ export def create_versions_file [
|
||||||
targetname: string = "versions"
|
targetname: string = "versions"
|
||||||
] {
|
] {
|
||||||
let target_name = if ($targetname | is-empty) { "versions" } else { $targetname }
|
let target_name = if ($targetname | is-empty) { "versions" } else { $targetname }
|
||||||
let provisioning_base = ($env.PROVISIONING? | default (get-base-path))
|
let provisioning_base = ($env.PROVISIONING? | default (get-config-base-path))
|
||||||
let versions_ncl = ($provisioning_base | path join "core" | path join "versions.ncl")
|
let versions_ncl = ($provisioning_base | path join "core" | path join "versions.ncl")
|
||||||
let versions_target = ($provisioning_base | path join "core" | path join $target_name)
|
let versions_target = ($provisioning_base | path join "core" | path join $target_name)
|
||||||
let providers_path = ($provisioning_base | path join "extensions" | path join "providers")
|
let providers_path = ($provisioning_base | path join "extensions" | path join "providers")
|
||||||
|
|
@ -74,10 +75,9 @@ export def create_versions_file [
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CORE TOOLS
|
# CORE TOOLS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
let nickel_result = (^nickel export $versions_ncl --format json | complete)
|
let json_data = (ncl-eval-soft $versions_ncl [] null)
|
||||||
|
|
||||||
if $nickel_result.exit_code == 0 {
|
if $json_data != null {
|
||||||
let json_data = ($nickel_result.stdout | from json)
|
|
||||||
let core_versions = ($json_data | get core_versions? | default [])
|
let core_versions = ($json_data | get core_versions? | default [])
|
||||||
|
|
||||||
for item in $core_versions {
|
for item in $core_versions {
|
||||||
|
|
@ -126,10 +126,9 @@ export def create_versions_file [
|
||||||
let provider_version_file = ($provider_dir | path join "nickel" | path join "version.ncl")
|
let provider_version_file = ($provider_dir | path join "nickel" | path join "version.ncl")
|
||||||
|
|
||||||
if ($provider_version_file | path exists) {
|
if ($provider_version_file | path exists) {
|
||||||
let provider_result = (^nickel export $provider_version_file --format json | complete)
|
let provider_data = (ncl-eval-soft $provider_version_file [] null)
|
||||||
|
|
||||||
if $provider_result.exit_code == 0 {
|
if $provider_data != null {
|
||||||
let provider_data = ($provider_result.stdout | from json)
|
|
||||||
let prov_name = ($provider_data | get name?)
|
let prov_name = ($provider_data | get name?)
|
||||||
let prov_version_obj = ($provider_data | get version?)
|
let prov_version_obj = ($provider_data | get version?)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
use ./mod.nu *
|
use ./mod.nu *
|
||||||
use ./detection.nu *
|
use ./detection.nu *
|
||||||
use ./validation.nu *
|
use ./validation.nu *
|
||||||
|
use ../utils/path-utils.nu *
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# INPUT HELPERS
|
# INPUT HELPERS
|
||||||
|
|
@ -560,61 +561,46 @@ export def run-minimal-setup [] {
|
||||||
# Run TypeDialog form via bash wrapper and return parsed result
|
# Run TypeDialog form via bash wrapper and return parsed result
|
||||||
# This pattern avoids TTY/input issues in Nushell's execution stack
|
# This pattern avoids TTY/input issues in Nushell's execution stack
|
||||||
def run-typedialog-form [
|
def run-typedialog-form [
|
||||||
wrapper_script: string
|
form_path: string
|
||||||
--backend: string = "tui"
|
--backend: string = "tui"
|
||||||
] {
|
] {
|
||||||
# Check if the wrapper script exists
|
# Guard 1: Check if plugin is available
|
||||||
if not ($wrapper_script | path exists) {
|
if (which typedialog | is-empty) {
|
||||||
print-setup-warning "TypeDialog wrapper not found. Using fallback prompts."
|
print-setup-error "TypeDialog plugin not available"
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: "TypeDialog wrapper not available"
|
error: "TypeDialog plugin not available"
|
||||||
use_fallback: true
|
use_fallback: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set backend environment variable
|
# Guard 2: Check if form file exists
|
||||||
$env.TYPEDIALOG_BACKEND = $backend
|
if not ($form_path | path exists) {
|
||||||
|
print-setup-error $"Form not found: ($form_path)"
|
||||||
# Run bash wrapper (handles TTY input properly)
|
|
||||||
let result = (do { bash $wrapper_script } | complete)
|
|
||||||
|
|
||||||
if $result.exit_code != 0 {
|
|
||||||
print-setup-error "TypeDialog wizard failed or was cancelled"
|
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: $result.stderr
|
error: $"Form not found: ($form_path)"
|
||||||
use_fallback: true
|
use_fallback: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read the generated JSON file
|
# Main logic: Call the nu_plugin_typedialog plugin directly
|
||||||
let json_output = ($wrapper_script | path dirname | path join "generated" | path join ($wrapper_script | path basename | str replace ".sh" "-result.json"))
|
# The plugin handles TTY properly via Nushell's native plugin protocol
|
||||||
|
let result = (typedialog form $form_path --backend $backend)
|
||||||
|
|
||||||
if not ($json_output | path exists) {
|
if ($result | is-empty) {
|
||||||
print-setup-warning "TypeDialog output not found. Using fallback."
|
# User cancelled the form
|
||||||
|
print-setup-warning "Setup wizard was cancelled"
|
||||||
return {
|
return {
|
||||||
success: false
|
success: false
|
||||||
error: "Output file not found"
|
error: "Form cancelled by user"
|
||||||
use_fallback: true
|
use_fallback: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse JSON output (no try-catch)
|
|
||||||
let parse_result = (do { open $json_output | from json } | complete)
|
|
||||||
if $parse_result.exit_code != 0 {
|
|
||||||
return {
|
|
||||||
success: false
|
|
||||||
error: "Failed to parse TypeDialog output"
|
|
||||||
use_fallback: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let values = ($parse_result.stdout)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
success: true
|
success: true
|
||||||
values: $values
|
values: $result
|
||||||
use_fallback: false
|
use_fallback: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -624,7 +610,7 @@ def run-typedialog-form [
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# Run setup wizard using TypeDialog - modern TUI experience
|
# Run setup wizard using TypeDialog - modern TUI experience
|
||||||
# Uses bash wrapper to handle TTY input properly
|
# Uses plugin directly for proper TTY handling
|
||||||
export def run-setup-wizard-interactive [
|
export def run-setup-wizard-interactive [
|
||||||
--backend: string = "tui"
|
--backend: string = "tui"
|
||||||
] {
|
] {
|
||||||
|
|
@ -637,9 +623,9 @@ export def run-setup-wizard-interactive [
|
||||||
print "╚═══════════════════════════════════════════════════════════════╝"
|
print "╚═══════════════════════════════════════════════════════════════╝"
|
||||||
print ""
|
print ""
|
||||||
|
|
||||||
# Run the TypeDialog-based wizard via bash wrapper
|
# Get TypeDialog form path with absolute resolution
|
||||||
let wrapper_script = "provisioning/core/shlib/setup-wizard-tty.sh"
|
let form_path = (get-typedialog-form-path "setup-wizard.toml")
|
||||||
let form_result = (run-typedialog-form $wrapper_script --backend $backend)
|
let form_result = (run-typedialog-form $form_path --backend $backend)
|
||||||
|
|
||||||
# If TypeDialog not available or failed, fall back to basic wizard
|
# If TypeDialog not available or failed, fall back to basic wizard
|
||||||
if (not $form_result.success or $form_result.use_fallback) {
|
if (not $form_result.success or $form_result.use_fallback) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
use std
|
use std
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
use ../utils/interface.nu *
|
use ../utils/interface.nu *
|
||||||
|
use ../utils/init.nu [get-provisioning-use-sops, get-workspace-path, get-provisioning-infra-path]
|
||||||
|
|
||||||
def find_file [
|
def find_file [
|
||||||
start_path: string
|
start_path: string
|
||||||
|
|
@ -34,8 +35,9 @@ export def run_cmd_sops [
|
||||||
let use_sops_value = (get-provisioning-use-sops | into string)
|
let use_sops_value = (get-provisioning-use-sops | into string)
|
||||||
let res = if ($use_sops_value | str contains "age") {
|
let res = if ($use_sops_value | str contains "age") {
|
||||||
if $env.SOPS_AGE_RECIPIENTS? != null {
|
if $env.SOPS_AGE_RECIPIENTS? != null {
|
||||||
# print $"SOPS_AGE_KEY_FILE=((get-sops-age-key-file)) ; sops ($str_cmd) --config ((find-sops-key)) --age ($env.SOPS_AGE_RECIPIENTS) ($source_path)"
|
(with-env { SOPS_AGE_KEY_FILE: (get-sops-age-key-file) } {
|
||||||
(^bash -c SOPS_AGE_KEY_FILE=((get-sops-age-key-file)) ; sops $str_cmd --config (find-sops-key) --age $env.SOPS_AGE_RECIPIENTS $source_path | complete )
|
do { ^sops $str_cmd --config (find-sops-key) --age $env.SOPS_AGE_RECIPIENTS $source_path } | complete
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
if $error_exit {
|
if $error_exit {
|
||||||
(throw-error $"🛑 Sops with age error" $"(_ansi red)no AGE_RECIPIENTS(_ansi reset) for (_ansi green)($source_path)(_ansi reset)"
|
(throw-error $"🛑 Sops with age error" $"(_ansi red)no AGE_RECIPIENTS(_ansi reset) for (_ansi green)($source_path)(_ansi reset)"
|
||||||
|
|
@ -243,7 +245,7 @@ export def get_def_age [
|
||||||
current_path: string
|
current_path: string
|
||||||
] {
|
] {
|
||||||
# Check if SOPS is configured for age encryption
|
# Check if SOPS is configured for age encryption
|
||||||
let use_sops = (get-provisioning-use-sops | tostring)
|
let use_sops = (get-provisioning-use-sops | into string)
|
||||||
if not ($use_sops | str contains "age") {
|
if not ($use_sops | str contains "age") {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -277,3 +279,16 @@ export def get_def_age [
|
||||||
}
|
}
|
||||||
($provisioning_kage | default "")
|
($provisioning_kage | default "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Return the SOPS config file path — env-var fast path, then filesystem search.
|
||||||
|
export def find-sops-key [] {
|
||||||
|
let from_env = ($env.PROVISIONING_SOPS? | default "")
|
||||||
|
if ($from_env | is-not-empty) { return $from_env }
|
||||||
|
let search_path = ($env.CURRENT_KLOUD_PATH? | default ($env.PROVISIONING_WORKSPACE_PATH? | default $env.PWD))
|
||||||
|
get_def_sops $search_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return the age private-key file path used for SOPS encryption/decryption.
|
||||||
|
export def get-sops-age-key-file [] {
|
||||||
|
$env.SOPS_AGE_KEY_FILE? | default ($env.PROVISIONING_KAGE? | default "")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# User Configuration Management Module
|
# User Configuration Management Module
|
||||||
# Manages central user configuration file for workspace switching and preferences
|
# Manages central user configuration file for workspace switching and preferences
|
||||||
|
|
||||||
|
use ../utils/nickel_processor.nu [ncl-eval-soft]
|
||||||
|
|
||||||
# Get path to user config file
|
# Get path to user config file
|
||||||
export def get-user-config-path [] {
|
export def get-user-config-path [] {
|
||||||
let user_config_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
let user_config_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
||||||
|
|
@ -371,13 +373,12 @@ export def get-workspace-default-infra [workspace_name: string] {
|
||||||
let ws_path = (get-workspace-path $workspace_name)
|
let ws_path = (get-workspace-path $workspace_name)
|
||||||
let ws_config_file = ([$ws_path "config" "provisioning.ncl"] | path join)
|
let ws_config_file = ([$ws_path "config" "provisioning.ncl"] | path join)
|
||||||
if ($ws_config_file | path exists) {
|
if ($ws_config_file | path exists) {
|
||||||
let result = (do -i {
|
let result = (ncl-eval-soft $ws_config_file [] null)
|
||||||
let ws_config = (^nickel export $ws_config_file --format json | from json)
|
|
||||||
let current_infra = ($ws_config.workspace_config.workspace.current_infra? | default null)
|
|
||||||
$current_infra
|
|
||||||
})
|
|
||||||
if ($result | is-not-empty) {
|
if ($result | is-not-empty) {
|
||||||
return $result
|
let current_infra = ($result.current_infra? | default null)
|
||||||
|
if ($current_infra | is-not-empty) {
|
||||||
|
return $current_infra
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
use ./logging.nu *
|
||||||
|
use ./interface.nu *
|
||||||
|
|
||||||
export def cleanup [
|
export def cleanup [
|
||||||
wk_path: string
|
wk_path: string
|
||||||
|
|
@ -6,7 +8,6 @@ export def cleanup [
|
||||||
if not (is-debug-enabled) and ($wk_path | path exists) {
|
if not (is-debug-enabled) and ($wk_path | path exists) {
|
||||||
rm --force --recursive $wk_path
|
rm --force --recursive $wk_path
|
||||||
} else {
|
} else {
|
||||||
#use utils/interface.nu _ansi
|
|
||||||
_print $"(_ansi default_dimmed)______________________(_ansi reset)"
|
_print $"(_ansi default_dimmed)______________________(_ansi reset)"
|
||||||
_print $"(_ansi default_dimmed)Work files not removed"
|
_print $"(_ansi default_dimmed)Work files not removed"
|
||||||
_print $"(_ansi default_dimmed)wk_path:(_ansi reset) ($wk_path)"
|
_print $"(_ansi default_dimmed)wk_path:(_ansi reset) ($wk_path)"
|
||||||
|
|
|
||||||
63
nulib/lib_provisioning/utils/command-registry.nu
Normal file
63
nulib/lib_provisioning/utils/command-registry.nu
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Module: Command Registry
|
||||||
|
# Purpose: Parse and query the commands registry Nickel file for command metadata
|
||||||
|
|
||||||
|
use ./nickel_processor.nu [ncl-eval]
|
||||||
|
|
||||||
|
# Parse commands registry Nickel file via JSON export
|
||||||
|
# Returns array of records with command metadata
|
||||||
|
def parse_registry [] {
|
||||||
|
let registry_file = (
|
||||||
|
if ($env.PROVISIONING? | is-not-empty) {
|
||||||
|
$env.PROVISIONING | path join "core" "nulib" "commands-registry.ncl"
|
||||||
|
} else {
|
||||||
|
"./provisioning/core/nulib/commands-registry.ncl"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ($registry_file | path exists) {
|
||||||
|
error make {msg: $"Registry file not found: ($registry_file)"}
|
||||||
|
}
|
||||||
|
|
||||||
|
(ncl-eval $registry_file []) | .commands
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get help category for a command (for commands requiring args)
|
||||||
|
export def get_help_category_for_command [command: string] {
|
||||||
|
let registry = (parse_registry)
|
||||||
|
let entry = ($registry | where { |r|
|
||||||
|
($r.command == $command) or ($r.aliases | any { |a| $a == $command })
|
||||||
|
} | first)
|
||||||
|
|
||||||
|
if ($entry | is-not-empty) and ($entry.requires_args == true) {
|
||||||
|
$entry.help_category
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if command requires arguments
|
||||||
|
export def command_requires_args [command: string] {
|
||||||
|
let registry = (parse_registry)
|
||||||
|
let entry = ($registry | where { |r|
|
||||||
|
($r.command == $command) or ($r.aliases | any { |a| $a == $command })
|
||||||
|
} | first)
|
||||||
|
|
||||||
|
if ($entry | is-not-empty) {
|
||||||
|
$entry.requires_args == true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get all commands that require arguments with their help categories
|
||||||
|
export def get_commands_requiring_args [] {
|
||||||
|
let registry = (parse_registry)
|
||||||
|
$registry
|
||||||
|
| where { |r| $r.requires_args == true and ($r.help_category | is-not-empty) }
|
||||||
|
| each { |r|
|
||||||
|
{
|
||||||
|
command: $r.command
|
||||||
|
help_category: $r.help_category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# Module: Error Handling Utilities
|
# Module: Error Handling Utilities
|
||||||
# Purpose: Centralized error handling, error messages, and exception management.
|
# Purpose: Centralized error handling, error messages, and exception management.
|
||||||
# Dependencies: None (core utility)
|
# Dependencies: logging
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
use ./logging.nu *
|
||||||
|
use ./interface.nu [_ansi]
|
||||||
|
|
||||||
export def throw-error [
|
export def throw-error [
|
||||||
error: string
|
error: string
|
||||||
|
|
@ -24,7 +26,7 @@ export def throw-error [
|
||||||
print $"DEBUG: Error code: ($code)"
|
print $"DEBUG: Error code: ($code)"
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($env.PROVISIONING_OUT | is-empty) {
|
if ($env.PROVISIONING_OUT? | default "" | is-empty) {
|
||||||
if $span == null and $context == null {
|
if $span == null and $context == null {
|
||||||
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
|
error make --unspanned { msg: ( $error + "\n" + $msg + $suggestion) }
|
||||||
} else if $span != null and (is-metadata-enabled) {
|
} else if $span != null and (is-metadata-enabled) {
|
||||||
|
|
@ -62,22 +64,3 @@ export def safe-execute [
|
||||||
$result.stdout
|
$result.stdout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export def try [
|
|
||||||
settings_data: record
|
|
||||||
defaults_data: record
|
|
||||||
] {
|
|
||||||
$settings_data.servers | each { |server|
|
|
||||||
_print ( $defaults_data.defaults | merge $server )
|
|
||||||
}
|
|
||||||
_print ($settings_data.servers | get hostname)
|
|
||||||
_print ($settings_data.servers | get 0).tasks
|
|
||||||
let zli_cfg = (open "resources/oci-reg/zli-cfg" | from json)
|
|
||||||
if $zli_cfg.sops? != null {
|
|
||||||
_print "Found"
|
|
||||||
} else {
|
|
||||||
_print "NOT Found"
|
|
||||||
}
|
|
||||||
let pos = 0
|
|
||||||
_print ($settings_data.servers | get $pos )
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# Intelligent Hints and Next-Step Guidance System
|
# Intelligent Hints and Next-Step Guidance System
|
||||||
# Provides contextual hints, documentation links, and next-step suggestions
|
# Provides contextual hints, documentation links, and next-step suggestions
|
||||||
|
|
||||||
|
use interface.nu [_ansi]
|
||||||
|
|
||||||
# Show next step suggestion after successful operation
|
# Show next step suggestion after successful operation
|
||||||
export def show-next-step [
|
export def show-next-step [
|
||||||
operation: string # Operation that just completed
|
operation: string # Operation that just completed
|
||||||
|
|
@ -24,10 +26,9 @@ export def show-next-step [
|
||||||
let service_name = ($ctx | get name? | default "service")
|
let service_name = ($ctx | get name? | default "service")
|
||||||
print $"\n(_ansi green_bold)✓ Taskserv '($service_name)' installed successfully!(_ansi reset)\n"
|
print $"\n(_ansi green_bold)✓ Taskserv '($service_name)' installed successfully!(_ansi reset)\n"
|
||||||
print $"(_ansi cyan_bold)Next steps:(_ansi reset)"
|
print $"(_ansi cyan_bold)Next steps:(_ansi reset)"
|
||||||
print $" 1. (_ansi blue)Verify installation:(_ansi reset) provisioning taskserv validate ($service_name)"
|
print $" 1. (_ansi blue)Dry-run check:(_ansi reset) provisioning taskserv create ($service_name) --check"
|
||||||
print $" 2. (_ansi blue)Create cluster:(_ansi reset) provisioning cluster create <cluster-name>"
|
print $" 2. (_ansi blue)Install more services:(_ansi reset) provisioning taskserv create <service>"
|
||||||
print $" (_ansi default_dimmed)Available clusters: buildkit, ci-cd, monitoring(_ansi reset)"
|
print $" 3. (_ansi blue)Create cluster:(_ansi reset) provisioning cluster create <cluster-name>"
|
||||||
print $" 3. (_ansi blue)Install more services:(_ansi reset) provisioning taskserv create <service>"
|
|
||||||
print $"\n(_ansi yellow)💡 Quick guide:(_ansi reset) provisioning guide from-scratch"
|
print $"\n(_ansi yellow)💡 Quick guide:(_ansi reset) provisioning guide from-scratch"
|
||||||
print $"(_ansi yellow)💡 Documentation:(_ansi reset) provisioning help infrastructure\n"
|
print $"(_ansi yellow)💡 Documentation:(_ansi reset) provisioning help infrastructure\n"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,90 @@
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
|
||||||
|
# Get the complete provisioning command arguments as a string
|
||||||
|
export def get-provisioning-args [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_ARGS? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the provisioning command name
|
||||||
|
export def get-provisioning-name [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_NAME? | default "provisioning"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the provisioning infrastructure path
|
||||||
|
export def get-provisioning-infra-path [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_INFRA_PATH? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the provisioning resources path
|
||||||
|
export def get-provisioning-resources [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_RESOURCES? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the provisioning URL
|
||||||
|
export def get-provisioning-url [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_URL? | default "https://provisioning.systems"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get whether SOPS encryption is enabled
|
||||||
|
export def get-provisioning-use-sops [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_USE_SOPS? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the effective workspace
|
||||||
|
export def get-effective-workspace [] : nothing -> string {
|
||||||
|
$env.CURRENT_WORKSPACE? | default "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get workspace path (defaults to effective workspace if not provided)
|
||||||
|
export def get-workspace-path [workspace?: string] : nothing -> string {
|
||||||
|
let ws = if ($workspace | is-empty) {
|
||||||
|
(get-effective-workspace)
|
||||||
|
} else {
|
||||||
|
$workspace
|
||||||
|
}
|
||||||
|
let ws_base = ($env.PROVISIONING_WORKSPACES? | default "")
|
||||||
|
if ($ws_base | is-not-empty) {
|
||||||
|
$ws_base | path join $ws
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect infrastructure from PWD
|
||||||
|
export def detect-infra-from-pwd [] : nothing -> string {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get work format (Nickel is the default post-migration)
|
||||||
|
export def get-work-format [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_WORK_FORMAT? | default "ncl"
|
||||||
|
}
|
||||||
|
|
||||||
export def show_titles [] {
|
export def show_titles [] {
|
||||||
if (detect_claude_code) { return false }
|
# Check if titles are disabled
|
||||||
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
|
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
|
||||||
if ($env.PROVISIONING_OUT | is-not-empty) { return }
|
if ($env.PROVISIONING_OUT? | is-not-empty) { return }
|
||||||
# Prevent double title display
|
|
||||||
if ($env.PROVISIONING_TITLES_SHOWN? | default false) { return }
|
if ($env.PROVISIONING_TITLES_SHOWN? | default false) { return }
|
||||||
|
|
||||||
|
# Mark as shown to prevent duplicates
|
||||||
$env.PROVISIONING_TITLES_SHOWN = true
|
$env.PROVISIONING_TITLES_SHOWN = true
|
||||||
_print $"(_ansi blue_bold)(open -r ((get-provisioning-resources) | path join "ascii.txt"))(_ansi reset)"
|
|
||||||
|
# Find ascii.txt from PROVISIONING_RESOURCES or PROVISIONING directory
|
||||||
|
let ascii_file = (
|
||||||
|
if ($env.PROVISIONING_RESOURCES? | is-not-empty) {
|
||||||
|
($env.PROVISIONING_RESOURCES | path join "ascii.txt")
|
||||||
|
} else if ($env.PROVISIONING? | is-not-empty) {
|
||||||
|
($env.PROVISIONING | path join "resources" | path join "ascii.txt")
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Display if file exists
|
||||||
|
if ($ascii_file | is-not-empty) and ($ascii_file | path exists) {
|
||||||
|
print $"(ansi blue_bold)(open -r $ascii_file)(ansi reset)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export def use_titles [ ] {
|
export def use_titles [ ] {
|
||||||
if ($env.PROVISIONING_NO_TITLES? | default false) { return false }
|
if ($env.PROVISIONING_NO_TITLES? | default false) { return false }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,48 @@
|
||||||
# Module: User Interface Utilities
|
# Module: User Interface Utilities
|
||||||
# Purpose: Provides terminal UI utilities: output formatting, prompts, spinners, and status displays.
|
# Purpose: Provides terminal UI utilities: output formatting, prompts, spinners, and status displays.
|
||||||
# Dependencies: error for error handling
|
# Dependencies: error for error handling, logging for debug utilities
|
||||||
|
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
use logging.nu [is-debug-enabled]
|
||||||
|
|
||||||
|
# Check if no-terminal mode is enabled
|
||||||
|
export def get-provisioning-no-terminal [] {
|
||||||
|
# Check environment variable first (use -o for optional in Nushell 0.106+)
|
||||||
|
let env_no_terminal = ($env | get -o PROVISIONING_NO_TERMINAL | default "false")
|
||||||
|
if ($env_no_terminal == "true") or ($env_no_terminal == "1") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check config setting
|
||||||
|
config-get "debug.no_terminal" false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get output format (json, yaml, table, etc.)
|
||||||
|
export def get-provisioning-out [] {
|
||||||
|
# Check environment variable first
|
||||||
|
let env_out = ($env | get -o PROVISIONING_OUT | default "")
|
||||||
|
if ($env_out | is-not-empty) {
|
||||||
|
return $env_out
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check config setting
|
||||||
|
config-get "output.format" ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set no-terminal mode
|
||||||
|
export def set-provisioning-no-terminal [value: bool] {
|
||||||
|
$env.PROVISIONING_NO_TERMINAL = $value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set output format
|
||||||
|
export def set-provisioning-out [value: string] {
|
||||||
|
$env.PROVISIONING_OUT = $value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get notification icon path
|
||||||
|
export def get-notify-icon [] : nothing -> string {
|
||||||
|
$env.PROVISIONING_NOTIFY_ICON? | default ""
|
||||||
|
}
|
||||||
|
|
||||||
export def _ansi [
|
export def _ansi [
|
||||||
arg?: string
|
arg?: string
|
||||||
|
|
@ -119,7 +159,7 @@ export def _print [
|
||||||
export def end_run [
|
export def end_run [
|
||||||
context: string
|
context: string
|
||||||
] {
|
] {
|
||||||
if ($env.PROVISIONING_OUT | is-not-empty) { return }
|
if ($env.PROVISIONING_OUT? | default "" | is-not-empty) { return }
|
||||||
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
|
if ($env.PROVISIONING_NO_TITLES? | default false) { return }
|
||||||
if (detect_claude_code) { return }
|
if (detect_claude_code) { return }
|
||||||
if (is-debug-enabled) {
|
if (is-debug-enabled) {
|
||||||
|
|
@ -146,7 +186,10 @@ export def show_clip_to [
|
||||||
] {
|
] {
|
||||||
if $show { _print $msg }
|
if $show { _print $msg }
|
||||||
if (is-terminal --stdout) {
|
if (is-terminal --stdout) {
|
||||||
clip_copy $msg $show
|
if ((version).installed_plugins | str contains "clipboard") {
|
||||||
|
$msg | clipboard copy
|
||||||
|
print $"(ansi default_dimmed)copied into clipboard now (ansi reset)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,10 +229,18 @@ export def desktop_run_notify [
|
||||||
(if $result.status { "✅ done " } else { $"🛑 fail ($result.error)" })
|
(if $result.status { "✅ done " } else { $"🛑 fail ($result.error)" })
|
||||||
} else { "" }
|
} else { "" }
|
||||||
let time_body = $"($body) ($msg) finished in ($total) "
|
let time_body = $"($body) ($msg) finished in ($total) "
|
||||||
( notify_msg $title $body $icon_path $time_body $timeout $task )
|
if ((version).installed_plugins | str contains "desktop_notifications") {
|
||||||
|
notify -s $title -t $time_body --timeout $time_out -i $icon_path
|
||||||
|
} else {
|
||||||
|
_print $"(_ansi blue)($title)(_ansi reset)\n(_ansi blue_bold)($time_body)(_ansi reset)"
|
||||||
|
}
|
||||||
return $result
|
return $result
|
||||||
} else {
|
} else {
|
||||||
( notify_msg $title $body $icon_path "" $timeout $task )
|
if ((version).installed_plugins | str contains "desktop_notifications") {
|
||||||
|
notify -s $title -t $body --timeout $time_out -i $icon_path
|
||||||
|
} else {
|
||||||
|
_print $"(_ansi blue)($title)(_ansi reset)\n(_ansi blue_bold)($body)(_ansi reset)"
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,32 @@ use ../config/accessor.nu *
|
||||||
|
|
||||||
# Check if debug mode is enabled
|
# Check if debug mode is enabled
|
||||||
export def is-debug-enabled [] {
|
export def is-debug-enabled [] {
|
||||||
(config-get "debug.enabled" false)
|
let raw = ($env.PROVISIONING_DEBUG? | default false)
|
||||||
|
let env_debug = if ($raw | describe) == "string" { $raw == "true" or $raw == "1" } else { $raw | into bool }
|
||||||
|
let config_debug = (config-get "debug.enabled" false)
|
||||||
|
$env_debug or $config_debug
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if debug-check mode is enabled (local task/service simulation)
|
||||||
|
export def is-debug-check-enabled [] {
|
||||||
|
$env.PROVISIONING_DEBUG_CHECK? | default false | into bool
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if metadata mode is enabled (for detailed error spans/metadata)
|
||||||
|
export def is-metadata-enabled [] {
|
||||||
|
let env_metadata = ($env.PROVISIONING_METADATA? | default false)
|
||||||
|
let config_metadata = (config-get "debug.metadata" false)
|
||||||
|
$env_metadata or $config_metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable debug mode
|
||||||
|
export def set-debug-enabled [value: bool] {
|
||||||
|
$env.PROVISIONING_DEBUG = $value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable metadata mode
|
||||||
|
export def set-metadata-enabled [value: bool] {
|
||||||
|
$env.PROVISIONING_METADATA = $value
|
||||||
}
|
}
|
||||||
|
|
||||||
export def log-info [
|
export def log-info [
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export use init.nu *
|
||||||
|
|
||||||
export use generate.nu *
|
export use generate.nu *
|
||||||
export use undefined.nu *
|
export use undefined.nu *
|
||||||
|
export use logging.nu *
|
||||||
|
|
||||||
export use qr.nu *
|
export use qr.nu *
|
||||||
export use ssh.nu *
|
export use ssh.nu *
|
||||||
|
|
|
||||||
95
nulib/lib_provisioning/utils/nickel_processor.nu
Normal file
95
nulib/lib_provisioning/utils/nickel_processor.nu
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Nickel file processor — plugin-backed evaluation with cache, with direct fallback.
|
||||||
|
|
||||||
|
# Raw export via ^nickel binary (no cache). Used internally as fallback.
|
||||||
|
export def process_nickel_export_raw [
|
||||||
|
src_file: string
|
||||||
|
out_format: string
|
||||||
|
]: nothing -> string {
|
||||||
|
let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
|
||||||
|
^nickel export $src_file --format $out_format --import-path $prov_root
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build the canonical import path list — used by callers that want explicit control.
|
||||||
|
#
|
||||||
|
# NOTE: After dropping import_paths from the cache key (plugin + daemon),
|
||||||
|
# the list passed here only affects cold-path nickel export invocations, NOT
|
||||||
|
# cache lookups. So mismatches between daemon and caller no longer cause misses.
|
||||||
|
export def default-ncl-paths [workspace: string = ""]: nothing -> list {
|
||||||
|
let ws = if ($workspace | is-not-empty) { $workspace } else { $env.PWD }
|
||||||
|
|
||||||
|
# Workspace-scoped paths (ontoref convention)
|
||||||
|
mut paths = [
|
||||||
|
($ws | path join ".ontology")
|
||||||
|
($ws | path join "adrs")
|
||||||
|
($ws | path join ".ontoref" | path join "ontology" | path join "schemas")
|
||||||
|
($ws | path join ".ontoref" | path join "adrs")
|
||||||
|
($ws | path join ".onref")
|
||||||
|
$ws
|
||||||
|
]
|
||||||
|
|
||||||
|
# $PROVISIONING
|
||||||
|
if ($env.PROVISIONING? | is-not-empty) { $paths = ($paths | append $env.PROVISIONING) }
|
||||||
|
|
||||||
|
# $NICKEL_IMPORT_PATH — colon-separated
|
||||||
|
if ($env.NICKEL_IMPORT_PATH? | is-not-empty) {
|
||||||
|
for entry in ($env.NICKEL_IMPORT_PATH | split row ":" | where { $in | is-not-empty }) {
|
||||||
|
$paths = ($paths | append $entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# $ONTOREF_ROOT — auto-discover or default macOS path
|
||||||
|
let ontoref_root = if ($env.ONTOREF_ROOT? | is-not-empty) {
|
||||||
|
$env.ONTOREF_ROOT
|
||||||
|
} else {
|
||||||
|
let home = ($env.HOME? | default "~" | path expand)
|
||||||
|
let mac_path = ($home | path join "Library" | path join "Application Support" | path join "ontoref")
|
||||||
|
let linux_path = ($home | path join ".local" | path join "share" | path join "ontoref")
|
||||||
|
if ($mac_path | path exists) {
|
||||||
|
$mac_path
|
||||||
|
} else {
|
||||||
|
if ($linux_path | path exists) { $linux_path } else { "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($ontoref_root | is-not-empty) {
|
||||||
|
$paths = ($paths | append [
|
||||||
|
($ontoref_root | path join "ontology")
|
||||||
|
($ontoref_root | path join "ontology" | path join "schemas")
|
||||||
|
($ontoref_root | path join "reflection")
|
||||||
|
($ontoref_root | path join "reflection" | path join "schemas")
|
||||||
|
($ontoref_root | path join "adrs")
|
||||||
|
$ontoref_root
|
||||||
|
] | flatten)
|
||||||
|
}
|
||||||
|
|
||||||
|
# De-duplicate preserving order (same as daemon)
|
||||||
|
$paths | reduce --fold [] {|it, acc|
|
||||||
|
if ($it in $acc) { $acc } else { $acc | append $it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Evaluate a Nickel file via the plugin (cached). Error propagates on failure.
|
||||||
|
#
|
||||||
|
# Equivalent to: ^nickel export --format json --import-path ... $path | from json
|
||||||
|
# but uses the nu_plugin_nickel cache, returning a Nu record/list directly.
|
||||||
|
export def ncl-eval [
|
||||||
|
path: string
|
||||||
|
import_paths: list = []
|
||||||
|
]: nothing -> any {
|
||||||
|
nickel-eval $path --import-path $import_paths
|
||||||
|
}
|
||||||
|
|
||||||
|
# Evaluate a Nickel file via the plugin (cached). Returns `fallback` on any error.
|
||||||
|
#
|
||||||
|
# Use for best-effort reads where failure is acceptable (e.g. optional NCL files).
|
||||||
|
# try/catch is valid for Nu plugin commands in Nu 0.111.0+.
|
||||||
|
export def ncl-eval-soft [
|
||||||
|
path: string
|
||||||
|
import_paths: list = []
|
||||||
|
fallback: any = null
|
||||||
|
]: nothing -> any {
|
||||||
|
try {
|
||||||
|
nickel-eval $path --import-path $import_paths
|
||||||
|
} catch {
|
||||||
|
$fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
61
nulib/lib_provisioning/utils/path-utils.nu
Normal file
61
nulib/lib_provisioning/utils/path-utils.nu
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Module: Path Resolution Utilities
|
||||||
|
# Purpose: Provides helpers for resolving provisioning project root and constructing absolute paths
|
||||||
|
# Used by: TypeDialog integration, setup wizard, auth forms
|
||||||
|
|
||||||
|
# Resolve provisioning project root with multiple fallback strategies
|
||||||
|
# Returns: The root directory that CONTAINS the provisioning folder
|
||||||
|
export def resolve-provisioning-root [] {
|
||||||
|
if "PROVISIONING_ROOT" in $env {
|
||||||
|
return $env.PROVISIONING_ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
if "PROVISIONING" in $env {
|
||||||
|
# PROVISIONING env var points to the provisioning folder itself
|
||||||
|
# We need its parent directory
|
||||||
|
let provisioning_dir = $env.PROVISIONING
|
||||||
|
let parent = ($provisioning_dir | path dirname)
|
||||||
|
|
||||||
|
# Verify the parent contains the provisioning folder
|
||||||
|
if ($parent | path join "provisioning" | path exists) {
|
||||||
|
return $parent
|
||||||
|
} else {
|
||||||
|
# PROVISIONING is already the project root
|
||||||
|
return $provisioning_dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find project root by walking up from current directory
|
||||||
|
# We're looking for the directory that CONTAINS the provisioning folder
|
||||||
|
mut search_dir = (pwd)
|
||||||
|
mut found_root = ""
|
||||||
|
|
||||||
|
# Try 10 levels up maximum to find project root
|
||||||
|
for i in (0..9) {
|
||||||
|
let provisioning_path = ($search_dir | path join "provisioning")
|
||||||
|
if ($provisioning_path | path exists) and (($provisioning_path | path type) == "dir") {
|
||||||
|
# Found the root - it's the parent of the provisioning dir
|
||||||
|
$found_root = $search_dir
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = ($search_dir | path dirname)
|
||||||
|
if $parent == $search_dir {
|
||||||
|
break # Reached filesystem root
|
||||||
|
}
|
||||||
|
|
||||||
|
$search_dir = $parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($found_root | is-empty) {
|
||||||
|
# Last resort: return current directory
|
||||||
|
pwd
|
||||||
|
} else {
|
||||||
|
$found_root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get TypeDialog form path with absolute resolution
|
||||||
|
export def get-typedialog-form-path [form_name: string] {
|
||||||
|
let provisioning_root = (resolve-provisioning-root)
|
||||||
|
$provisioning_root | path join "provisioning" ".typedialog" "core" "forms" $form_name
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,22 @@
|
||||||
use ../config/accessor.nu *
|
use ../config/accessor.nu *
|
||||||
|
|
||||||
|
# Display QR code for URL using qr_maker plugin or fallback
|
||||||
|
def show_qr [url: string]: nothing -> nothing {
|
||||||
|
let has_qr_plugin = ((version).installed_plugins | str contains "qr_maker")
|
||||||
|
|
||||||
|
if $has_qr_plugin {
|
||||||
|
print ($url | to qr)
|
||||||
|
} else {
|
||||||
|
let qr_path = ((get-provisioning-resources) | path join "qrs" | path join ($url | path basename))
|
||||||
|
if ($qr_path | path exists) {
|
||||||
|
print (open -r $qr_path)
|
||||||
|
} else {
|
||||||
|
print $"(ansi blue_reverse)($url)(ansi reset)"
|
||||||
|
print $"(ansi purple)($url)(ansi reset)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export def "make_qr" [
|
export def "make_qr" [
|
||||||
url?: string
|
url?: string
|
||||||
] {
|
] {
|
||||||
|
|
|
||||||
84
nulib/lib_provisioning/utils/script-compression.nu
Normal file
84
nulib/lib_provisioning/utils/script-compression.nu
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Script compression utilities for secure transmission
|
||||||
|
# Compresses template path, variables, and script as a complete auditable unit
|
||||||
|
|
||||||
|
# Compress complete workflow data (template + vars + script)
|
||||||
|
export def compress-workflow [template_path: string, template_vars: record, script: string]: nothing -> record {
|
||||||
|
# Create temporary directory
|
||||||
|
let timestamp_hash = ((date now | format date "%Y%m%d_%H%M%S") | hash sha256 | str substring 0..8)
|
||||||
|
let temp_dir = $"/tmp/workflow_($timestamp_hash)"
|
||||||
|
^mkdir -p $temp_dir
|
||||||
|
|
||||||
|
# 1. Compress template_vars (JSON)
|
||||||
|
let vars_file = ($temp_dir + "/vars.json")
|
||||||
|
let vars_json = ($template_vars | to json)
|
||||||
|
$vars_json | save -f $vars_file
|
||||||
|
let vars_original_size = ($vars_json | str length)
|
||||||
|
|
||||||
|
# 2. Compress script
|
||||||
|
let script_file = ($temp_dir + "/script.sh")
|
||||||
|
$script | save -f $script_file
|
||||||
|
let script_original_size = ($script | str length)
|
||||||
|
|
||||||
|
# 3. Create manifest with template_path
|
||||||
|
let manifest_file = ($temp_dir + "/manifest.json")
|
||||||
|
{
|
||||||
|
template_path: $template_path
|
||||||
|
timestamp: ((date now) | format date "%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
} | to json | save -f $manifest_file
|
||||||
|
|
||||||
|
# 4. Combine all into single archive
|
||||||
|
let total_original = ($vars_original_size + $script_original_size)
|
||||||
|
let archive_file = ($temp_dir + "/workflow.tar.gz")
|
||||||
|
|
||||||
|
^tar -czf $archive_file -C $temp_dir manifest.json vars.json script.sh
|
||||||
|
|
||||||
|
# 5. Encode to base64
|
||||||
|
let tar_content = (open -r $archive_file)
|
||||||
|
let compressed_data = ($tar_content | ^base64)
|
||||||
|
|
||||||
|
# Get compressed size using base64 encoded output (approximation)
|
||||||
|
let compressed_size = ($compressed_data | str length)
|
||||||
|
|
||||||
|
# Calculate ratio
|
||||||
|
let compression_ratio = ($compressed_size / $total_original)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
^rm -rf $temp_dir
|
||||||
|
|
||||||
|
{
|
||||||
|
template_path: $template_path
|
||||||
|
script_compressed: $compressed_data
|
||||||
|
script_encoding: "tar+gzip+base64"
|
||||||
|
original_size: $total_original
|
||||||
|
compressed_size: $compressed_size
|
||||||
|
compression_ratio: $compression_ratio
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Decompress workflow (for verification/testing)
|
||||||
|
export def decompress-workflow [script_compressed: string]: nothing -> record {
|
||||||
|
let timestamp_hash = ((date now | format date "%Y%m%d_%H%M%S") | hash sha256 | str substring 0..8)
|
||||||
|
let temp_dir = $"/tmp/workflow_decompress_($timestamp_hash)"
|
||||||
|
^mkdir -p $temp_dir
|
||||||
|
|
||||||
|
# Decode from base64
|
||||||
|
let decoded = (echo $script_compressed | ^base64 -d)
|
||||||
|
|
||||||
|
# Extract tar.gz
|
||||||
|
echo $decoded | ^tar -xzf - -C $temp_dir
|
||||||
|
|
||||||
|
# Read files
|
||||||
|
let manifest = (open ($temp_dir + "/manifest.json"))
|
||||||
|
let vars = (open ($temp_dir + "/vars.json"))
|
||||||
|
let script = (open -r ($temp_dir + "/script.sh"))
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
^rm -rf $temp_dir
|
||||||
|
|
||||||
|
{
|
||||||
|
template_path: $manifest.template_path
|
||||||
|
template_vars: $vars
|
||||||
|
script: $script
|
||||||
|
timestamp: $manifest.timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
255
nulib/lib_provisioning/utils/service-check.nu
Normal file
255
nulib/lib_provisioning/utils/service-check.nu
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
# Module: Service Availability Check Utilities
|
||||||
|
# Purpose: Reusable patterns for checking service availability before making requests
|
||||||
|
# Guidelines: Follows .claude/guidelines/provisioning.md - Service Check Pattern
|
||||||
|
#
|
||||||
|
# Features:
|
||||||
|
# - Check individual service availability
|
||||||
|
# - Check all essential services (cascade failure detection)
|
||||||
|
# - Check external dependencies (database, OCI registries, Git sources)
|
||||||
|
# - Clean error messages with short aliases
|
||||||
|
# - No stack traces (uses print + return, not error make)
|
||||||
|
|
||||||
|
use ../platform/target.nu *
|
||||||
|
use ../platform/health.nu *
|
||||||
|
use ../platform/service-manager.nu *
|
||||||
|
|
||||||
|
# Check external services locally (avoiding startup.nu import due to syntax errors in that file)
|
||||||
|
def check-external-services-internal [external_config: record]: nothing -> list {
|
||||||
|
let db = ($external_config.database? | default {backend: "filesystem"})
|
||||||
|
let oci_registries = ($external_config.oci_registries? | default [])
|
||||||
|
let git_sources = ($external_config.git_sources? | default [])
|
||||||
|
|
||||||
|
mut results = []
|
||||||
|
|
||||||
|
# Check database
|
||||||
|
if ($db.backend? | default "filesystem") == "filesystem" {
|
||||||
|
let path = ($db.path? | default "~/.provisioning/data")
|
||||||
|
let expanded_path = if ($path | str starts-with "~") {
|
||||||
|
$"($env.HOME)/($path | str substring 1..)"
|
||||||
|
} else {
|
||||||
|
$path
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($expanded_path | path exists) {
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "✓"
|
||||||
|
message: $"Filesystem storage available at ($expanded_path)"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
$results = ($results | append {
|
||||||
|
service: "database"
|
||||||
|
backend: $db.backend
|
||||||
|
status: "✗"
|
||||||
|
message: $"Path does not exist: ($expanded_path)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$results
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a service is available by verifying port is listening
|
||||||
|
# Returns: { available: bool, port: string, message: string }
|
||||||
|
export def check-service-available [
|
||||||
|
service_url: string # Service URL (e.g., "http://localhost:9011")
|
||||||
|
service_name: string # Human-readable service name (e.g., "Orchestrator")
|
||||||
|
]: nothing -> record {
|
||||||
|
# Extract port from URL
|
||||||
|
let parsed = ($service_url | parse "http://{host}:{port}")
|
||||||
|
let port = if ($parsed | is-empty) {
|
||||||
|
"unknown"
|
||||||
|
} else {
|
||||||
|
($parsed | get port.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if port is listening (macOS: lsof, Linux: netstat fallback)
|
||||||
|
# Using do { } | complete pattern per Nushell guidelines (NO try-catch)
|
||||||
|
let port_check = (do { ^lsof -i :($port) -P -n | ^grep LISTEN } | complete)
|
||||||
|
let is_listening = ($port_check.exit_code == 0)
|
||||||
|
|
||||||
|
if $is_listening {
|
||||||
|
{
|
||||||
|
available: true,
|
||||||
|
port: $port,
|
||||||
|
message: $"($service_name) is available on port ($port)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
available: false,
|
||||||
|
port: $port,
|
||||||
|
message: $"($service_name) is not available on port ($port)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check external services (database, OCI registries, Git sources)
|
||||||
|
# Returns list of external service statuses
|
||||||
|
export def check-external-services-status []: nothing -> list {
|
||||||
|
let external_services = (get-external-services)
|
||||||
|
|
||||||
|
if ($external_services | is-empty) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
# get-external-services returns a table/list, we need to process each item
|
||||||
|
# For now, return simplified status based on what we can check
|
||||||
|
$external_services | each {|svc|
|
||||||
|
{
|
||||||
|
service: $svc.name
|
||||||
|
backend: ($svc.srvc? | default "external")
|
||||||
|
status: "✓"
|
||||||
|
message: $"External service: ($svc.name) at ($svc.url)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check all platform services and return their status
|
||||||
|
# Returns list of {name: string, status: string, priority: int}
|
||||||
|
export def check-platform-services-status []: nothing -> list {
|
||||||
|
let services = (get-enabled-services)
|
||||||
|
|
||||||
|
$services | each {|svc|
|
||||||
|
let healthy = (check-service-health $svc.name)
|
||||||
|
{
|
||||||
|
name: $svc.name,
|
||||||
|
status: (if $healthy { "healthy" } else { "unhealthy" }),
|
||||||
|
priority: $svc.priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show cascade failure report - prints static help without expensive service scanning
|
||||||
|
export def show-cascade-failure-report [failed_service: string]: nothing -> nothing {
|
||||||
|
print ""
|
||||||
|
print $"❌ ($failed_service) is not running."
|
||||||
|
print ""
|
||||||
|
print "Start all platform services:"
|
||||||
|
print " provisioning platform start"
|
||||||
|
print " prvng plat start # short alias"
|
||||||
|
print ""
|
||||||
|
print "Check service status:"
|
||||||
|
print " provisioning platform status"
|
||||||
|
print " prvng plat st # short alias"
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify service availability and fail with clean error message if not available
|
||||||
|
# This function prints error and returns error status (NO stack trace)
|
||||||
|
# Usage: Call this BEFORE making HTTP requests to services
|
||||||
|
export def verify-service-or-fail [
|
||||||
|
service_url: string # Service URL (e.g., "http://localhost:9011")
|
||||||
|
service_name: string # Human-readable service name (e.g., "Orchestrator")
|
||||||
|
--check-command: string = "" # Full command to check status
|
||||||
|
--check-alias: string = "" # Short alias for check (e.g., "prvng ps")
|
||||||
|
--start-command: string = "" # Full command to start service
|
||||||
|
--start-alias: string = "" # Short alias for start (e.g., "prvng start orchestrator")
|
||||||
|
]: nothing -> record {
|
||||||
|
let check_result = (check-service-available $service_url $service_name)
|
||||||
|
|
||||||
|
if not $check_result.available {
|
||||||
|
# Print clean error message WITHOUT stack trace (NO error make)
|
||||||
|
print $"❌ ($service_name) not available at ($service_url)"
|
||||||
|
print ""
|
||||||
|
print $"Connection refused - ($service_name) is not running on port ($check_result.port)."
|
||||||
|
print ""
|
||||||
|
|
||||||
|
# Show cascade failure report (external services + platform services)
|
||||||
|
show-cascade-failure-report $service_name
|
||||||
|
|
||||||
|
# Show commands with aliases
|
||||||
|
if ($check_command | is-not-empty) {
|
||||||
|
print "To check service status:"
|
||||||
|
print $" ($check_command)"
|
||||||
|
if ($check_alias | is-not-empty) {
|
||||||
|
print $" ($check_alias) # short alias"
|
||||||
|
}
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($start_command | is-not-empty) {
|
||||||
|
print "To start service:"
|
||||||
|
print $" ($start_command)"
|
||||||
|
if ($start_alias | is-not-empty) {
|
||||||
|
print $" ($start_alias) # short alias"
|
||||||
|
}
|
||||||
|
print ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print $"Current endpoint: ($service_url)"
|
||||||
|
print "If using a custom endpoint, verify it with: --orchestrator <url>"
|
||||||
|
|
||||||
|
# Return error status WITHOUT stack trace
|
||||||
|
return {status: "error", message: $"($service_name) not available"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service is available
|
||||||
|
return {status: "ok", message: $"($service_name) is available"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lightweight check - just returns boolean, no error message
|
||||||
|
export def is-service-available [
|
||||||
|
service_url: string # Service URL
|
||||||
|
service_name: string # Service name
|
||||||
|
]: nothing -> bool {
|
||||||
|
let check_result = (check-service-available $service_url $service_name)
|
||||||
|
$check_result.available
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if provisioning_daemon is available (CRITICAL - required for ALL operations)
|
||||||
|
# Returns: { available: bool, port: int }
|
||||||
|
export def check-daemon-availability []: nothing -> record {
|
||||||
|
# Get daemon configuration
|
||||||
|
let daemon_config = (get-deployment-service-config "provisioning_daemon")
|
||||||
|
let daemon_port = ($daemon_config.server?.port? | default 9095)
|
||||||
|
|
||||||
|
# Check if daemon port is listening
|
||||||
|
let port_check = (do { ^lsof -i :($daemon_port) -P -n | ^grep LISTEN } | complete)
|
||||||
|
let is_available = ($port_check.exit_code == 0)
|
||||||
|
|
||||||
|
{
|
||||||
|
available: $is_available
|
||||||
|
port: $daemon_port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify daemon is available - CRITICAL prerequisite for ALL operations
|
||||||
|
# Blocks execution if daemon is not available (except for help, platform, setup)
|
||||||
|
# Returns error status if daemon unavailable
|
||||||
|
export def verify-daemon-or-block [
|
||||||
|
operation: string # Operation being attempted (for error message)
|
||||||
|
]: nothing -> record {
|
||||||
|
let daemon_check = (check-daemon-availability)
|
||||||
|
|
||||||
|
if not $daemon_check.available {
|
||||||
|
print ""
|
||||||
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
print "❌ CRITICAL: provisioning_daemon not available"
|
||||||
|
print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
print ""
|
||||||
|
print $"The provisioning daemon is required for operation: ($operation)"
|
||||||
|
print $"Daemon is not listening on port ($daemon_check.port)"
|
||||||
|
print ""
|
||||||
|
print "The daemon is a CRITICAL component - all operations require it."
|
||||||
|
print ""
|
||||||
|
print "To check daemon status:"
|
||||||
|
print " provisioning platform status"
|
||||||
|
print " prvng plat st # short alias"
|
||||||
|
print ""
|
||||||
|
print "To start the daemon:"
|
||||||
|
print " provisioning platform start provisioning_daemon"
|
||||||
|
print " prvng plat start provisioning_daemon # short alias"
|
||||||
|
print ""
|
||||||
|
print "Allowed operations without daemon:"
|
||||||
|
print " • help / -h / --help - View help"
|
||||||
|
print " • platform <cmd> - Manage platform services"
|
||||||
|
print " • setup - Initial setup"
|
||||||
|
print ""
|
||||||
|
|
||||||
|
return {status: "error", message: "provisioning_daemon not available"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Daemon is available
|
||||||
|
return {status: "ok", message: "provisioning_daemon is available"}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue