chore: release 1.0.11 - nu script cleanup & refactoring + i18n fluentd
- Documented Fluent-based i18n system with locale detection - Bumped version from 1.0.10 to 1.0.11
This commit is contained in:
parent
93625d6290
commit
eb20fec7de
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,7 +5,7 @@
|
||||
.coder
|
||||
.migration
|
||||
.zed
|
||||
ai_demo.nu
|
||||
# ai_demo.nu
|
||||
CLAUDE.md
|
||||
.cache
|
||||
.coder
|
||||
|
||||
@ -102,16 +102,18 @@ repos:
|
||||
types: [markdown]
|
||||
stages: [pre-commit]
|
||||
|
||||
# NOTE: Disabled - markdownlint-cli2 already catches syntax issues
|
||||
# This script is redundant and causing false positives
|
||||
# - id: check-malformed-fences
|
||||
# name: Check malformed closing fences
|
||||
# entry: bash -c 'cd .. && nu scripts/check-malformed-fences.nu $(git diff --cached --name-only --diff-filter=ACM | grep "\.md$" | grep -v ".coder/" | grep -v ".claude/" | grep -v "old_config/" | tr "\n" " ")'
|
||||
# language: system
|
||||
# types: [markdown]
|
||||
# pass_filenames: false
|
||||
# stages: [pre-commit]
|
||||
# exclude: ^\.coder/|^\.claude/|^old_config/
|
||||
# CRITICAL: markdownlint-cli2 MD040 only checks opening fences for language.
|
||||
# It does NOT catch malformed closing fences (e.g., ```plaintext) - CommonMark violation.
|
||||
# This hook is ESSENTIAL to prevent malformed closing fences from entering the repo.
|
||||
# See: .markdownlint-cli2.jsonc line 22-24 for details.
|
||||
- id: check-malformed-fences
|
||||
name: Check malformed closing fences (CommonMark)
|
||||
entry: bash -c 'cd .. && nu scripts/check-malformed-fences.nu $(git diff --cached --name-only --diff-filter=ACM | grep "\.md$" | grep -v ".coder/" | grep -v ".claude/" | grep -v "old_config/" | tr "\n" " ")'
|
||||
language: system
|
||||
types: [markdown]
|
||||
pass_filenames: false
|
||||
stages: [pre-commit]
|
||||
exclude: ^\.coder/|^\.claude/|^old_config/
|
||||
|
||||
# ============================================================================
|
||||
# General Pre-commit Hooks
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@ -1,6 +1,6 @@
|
||||
# Provisioning Core - Changelog
|
||||
|
||||
**Date**: 2026-01-08
|
||||
**Date**: 2026-01-14
|
||||
**Repository**: provisioning/core
|
||||
**Status**: Nickel IaC (PRIMARY)
|
||||
|
||||
@ -8,8 +8,67 @@
|
||||
|
||||
## 📋 Summary
|
||||
|
||||
Core system with Nickel as primary IaC: CLI enhancements, Nushell library refactoring for schema support,
|
||||
config loader for Nickel evaluation, and comprehensive infrastructure automation.
|
||||
Core system with Nickel as primary IaC: Terminology migration from cluster to taskserv throughout codebase,
|
||||
Nushell library refactoring for improved ANSI output formatting, and enhanced handler modules for infrastructure operations.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Latest Release (2026-01-14)
|
||||
|
||||
### Terminology Migration: Cluster → Taskserv
|
||||
|
||||
**Scope**: Complete refactoring across nulib/ modules to standardize on taskserv nomenclature
|
||||
|
||||
**Files Updated**:
|
||||
- `nulib/clusters/handlers.nu` - Handler signature updates, ANSI formatting improvements
|
||||
- `nulib/clusters/run.nu` - Function parameter and path updates (+326 lines modified)
|
||||
- `nulib/clusters/utils.nu` - Utility function updates (+144 lines modified)
|
||||
- `nulib/clusters/discover.nu` - Discovery module refactoring
|
||||
- `nulib/clusters/load.nu` - Configuration loader updates
|
||||
- `nulib/ai/query_processor.nu` - AI integration updates
|
||||
- `nulib/api/routes.nu` - API routing adjustments
|
||||
- `nulib/api/server.nu` - Server module updates
|
||||
- `.pre-commit-config.yaml` - Pre-commit hook updates
|
||||
|
||||
**Changes**:
|
||||
- Updated function parameters: `server_cluster_path` → `server_taskserv_path`
|
||||
- Updated record fields: `defs.cluster.name` → `defs.taskserv.name`
|
||||
- Enhanced output formatting with consistent ANSI styling (yellow_bold, default_dimmed, purple_bold)
|
||||
- Improved function documentation and import organization
|
||||
- Pre-commit configuration refinements
|
||||
|
||||
**Rationale**: Taskserv better reflects the service-oriented nature of infrastructure components and improves semantic clarity throughout the codebase.
|
||||
|
||||
### i18n/Localization System
|
||||
|
||||
**New Feature**: Fluent i18n integration for internationalized help system
|
||||
|
||||
**Implementation**:
|
||||
- `nulib/main_provisioning/help_system_fluent.nu` - Fluent-based i18n framework
|
||||
- Active locale detection from `LANG` environment variable
|
||||
- Fallback to English (en-US) for missing translations
|
||||
- Fluent catalog parsing: `locale/{locale}/help.ftl`
|
||||
- Locale format conversion: `es_ES.UTF-8` → `es-ES`
|
||||
|
||||
**Features**:
|
||||
- Automatic locale detection from system LANG
|
||||
- Fluent catalog format support for translations
|
||||
- Graceful fallback mechanism
|
||||
- Category-based color formatting (infrastructure, orchestration, development, etc.)
|
||||
- Tab-separated help column formatting
|
||||
|
||||
---
|
||||
|
||||
## 📋 Version History
|
||||
|
||||
### v1.0.10 (Previous Release)
|
||||
- Stable release with Nickel IaC support
|
||||
- Base version with core CLI and library system
|
||||
|
||||
### v1.0.11 (Current - 2026-01-14)
|
||||
- **Cluster → Taskserv** terminology migration
|
||||
- **Fluent i18n** system documentation
|
||||
- Enhanced ANSI output formatting
|
||||
|
||||
---
|
||||
|
||||
@ -175,6 +234,6 @@ Service definitions and configurations
|
||||
---
|
||||
|
||||
**Status**: Production
|
||||
**Date**: 2026-01-08
|
||||
**Date**: 2026-01-14
|
||||
**Repository**: provisioning/core
|
||||
**Version**: 5.0.0
|
||||
**Version**: 1.0.11
|
||||
|
||||
67
README.md
67
README.md
@ -25,7 +25,7 @@ The Core Engine provides:
|
||||
|
||||
## Project Structure
|
||||
|
||||
```plaintext
|
||||
```text
|
||||
provisioning/core/
|
||||
├── cli/ # Command-line interface
|
||||
│ └── provisioning # Main CLI entry point (211 lines, 84% reduction)
|
||||
@ -74,7 +74,7 @@ export PATH="$PATH:/path/to/project-provisioning/provisioning/core/cli"
|
||||
|
||||
Verify installation:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning version
|
||||
provisioning help
|
||||
```
|
||||
@ -124,13 +124,13 @@ provisioning server ssh hostname-01
|
||||
|
||||
For fastest command reference:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning sc
|
||||
```
|
||||
|
||||
For complete guides:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning guide from-scratch # Complete deployment guide
|
||||
provisioning guide quickstart # Command shortcuts reference
|
||||
provisioning guide customize # Customization patterns
|
||||
@ -199,6 +199,38 @@ provisioning workflow list
|
||||
provisioning workflow status <id>
|
||||
```
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
### Fluent-based Localization
|
||||
|
||||
The help system supports multiple languages using the Fluent catalog format:
|
||||
|
||||
```bash
|
||||
# Automatic locale detection from LANG environment variable
|
||||
export LANG=es_ES.UTF-8
|
||||
provisioning help # Shows Spanish help if es-ES catalog exists
|
||||
|
||||
# Falls back to en-US if translation not available
|
||||
export LANG=fr_FR.UTF-8
|
||||
provisioning help # Shows French help if fr-FR exists, otherwise English
|
||||
```
|
||||
|
||||
**Catalog Structure**:
|
||||
|
||||
```text
|
||||
provisioning/locales/
|
||||
├── en-US/
|
||||
│ └── help.ftl # English help strings
|
||||
├── es-ES/
|
||||
│ └── help.ftl # Spanish help strings
|
||||
└── de-DE/
|
||||
└── help.ftl # German help strings
|
||||
```
|
||||
|
||||
**Supported Locales**: en-US (default), with framework ready for es-ES, fr-FR, de-DE, etc.
|
||||
|
||||
---
|
||||
|
||||
## CLI Architecture
|
||||
|
||||
### Modular Design
|
||||
@ -234,7 +266,7 @@ See complete reference: `provisioning sc` or `provisioning guide quickstart`
|
||||
|
||||
Help works in both directions:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning help workspace # ✅
|
||||
provisioning workspace help # ✅ Same result
|
||||
provisioning ws help # ✅ Shortcut also works
|
||||
@ -405,14 +437,14 @@ When contributing to the Core Engine:
|
||||
|
||||
**Missing environment variables:**
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning env # Check current configuration
|
||||
provisioning validate config # Validate configuration files
|
||||
```
|
||||
|
||||
**Nickel schema errors:**
|
||||
|
||||
```bash
|
||||
```text
|
||||
nickel fmt <file>.ncl # Format Nickel file
|
||||
nickel eval <file>.ncl # Evaluate Nickel schema
|
||||
nickel typecheck <file>.ncl # Type check schema
|
||||
@ -420,7 +452,7 @@ nickel typecheck <file>.ncl # Type check schema
|
||||
|
||||
**Provider authentication:**
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning providers # List available providers
|
||||
provisioning show settings # View provider configuration
|
||||
```
|
||||
@ -429,13 +461,13 @@ provisioning show settings # View provider configuration
|
||||
|
||||
Enable verbose logging:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning --debug <command>
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning help # Show main help
|
||||
provisioning help <category> # Category-specific help
|
||||
provisioning <command> help # Command-specific help
|
||||
@ -446,7 +478,7 @@ provisioning guide list # List all guides
|
||||
|
||||
Check system versions:
|
||||
|
||||
```bash
|
||||
```text
|
||||
provisioning version # Show all versions
|
||||
provisioning nuinfo # Nushell information
|
||||
```
|
||||
@ -457,5 +489,16 @@ See project root LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
## Recent Updates
|
||||
|
||||
### 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
|
||||
- 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
|
||||
**Last Updated**: 2026-01-08
|
||||
**Last Updated**: 2026-01-14
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# Info: Script to run Provisioning
|
||||
# Author: JesusPerezLorenzo
|
||||
# Release: 1.0.10
|
||||
# Date: 2025-10-02
|
||||
# Release: 1.0.11
|
||||
# Date: 2026-01-14
|
||||
|
||||
set +o errexit
|
||||
set +o pipefail
|
||||
@ -145,6 +145,8 @@ fi
|
||||
# Help commands (uses help_minimal.nu)
|
||||
if [ -z "$1" ] || [ "$1" = "help" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "--helpinfo" ]; then
|
||||
category="${2:-}"
|
||||
# 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 '$category' | print" 2>/dev/null
|
||||
exit $?
|
||||
fi
|
||||
|
||||
@ -26,7 +26,7 @@ export def process_query [
|
||||
--agent: string = "auto"
|
||||
--format: string = "json"
|
||||
--max_results: int = 100
|
||||
]: string -> any {
|
||||
] {
|
||||
|
||||
print $"🤖 Processing query: ($query)"
|
||||
|
||||
@ -80,7 +80,7 @@ export def process_query [
|
||||
}
|
||||
|
||||
# Analyze query intent using NLP patterns
|
||||
def analyze_query_intent [query: string]: string -> record {
|
||||
def analyze_query_intent [query: string] {
|
||||
let lower_query = ($query | str downcase)
|
||||
|
||||
# Infrastructure status patterns
|
||||
@ -153,7 +153,7 @@ def analyze_query_intent [query: string]: string -> record {
|
||||
}
|
||||
|
||||
# Extract entities from query text
|
||||
def extract_entities [query: string, entity_types: list<string>]: nothing -> list<string> {
|
||||
def extract_entities [query: string, entity_types: list<string>] {
|
||||
let lower_query = ($query | str downcase)
|
||||
mut entities = []
|
||||
|
||||
@ -183,7 +183,7 @@ def extract_entities [query: string, entity_types: list<string>]: nothing -> lis
|
||||
}
|
||||
|
||||
# Select optimal agent based on query type and entities
|
||||
def select_optimal_agent [query_type: string, entities: list<string>]: nothing -> string {
|
||||
def select_optimal_agent [query_type: string, entities: list<string>] {
|
||||
match $query_type {
|
||||
"infrastructure_status" => "infrastructure_monitor"
|
||||
"performance_analysis" => "performance_analyzer"
|
||||
@ -204,7 +204,7 @@ def process_infrastructure_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "🏗️ Analyzing infrastructure status..."
|
||||
|
||||
@ -243,7 +243,7 @@ def process_performance_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "⚡ Analyzing performance metrics..."
|
||||
|
||||
@ -283,7 +283,7 @@ def process_cost_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "💰 Analyzing cost optimization opportunities..."
|
||||
|
||||
@ -323,7 +323,7 @@ def process_security_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "🛡️ Performing security analysis..."
|
||||
|
||||
@ -364,7 +364,7 @@ def process_predictive_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "🔮 Generating predictive analysis..."
|
||||
|
||||
@ -404,7 +404,7 @@ def process_troubleshooting_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "🔧 Analyzing troubleshooting data..."
|
||||
|
||||
@ -445,7 +445,7 @@ def process_general_query [
|
||||
agent: string
|
||||
format: string
|
||||
max_results: int
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print "🤖 Processing general infrastructure query..."
|
||||
|
||||
@ -471,7 +471,7 @@ def process_general_query [
|
||||
}
|
||||
|
||||
# Helper functions for data collection
|
||||
def collect_system_metrics []: nothing -> record {
|
||||
def collect_system_metrics [] {
|
||||
{
|
||||
cpu: (sys cpu | get cpu_usage | math avg)
|
||||
memory: (sys mem | get used)
|
||||
@ -480,7 +480,7 @@ def collect_system_metrics []: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
def get_servers_status []: nothing -> list<record> {
|
||||
def get_servers_status [] {
|
||||
# Mock data - in real implementation would query actual infrastructure
|
||||
[
|
||||
{ name: "web-01", status: "healthy", cpu: 45, memory: 67 }
|
||||
@ -490,7 +490,7 @@ def get_servers_status []: nothing -> list<record> {
|
||||
}
|
||||
|
||||
# Insight generation functions
|
||||
def generate_infrastructure_insights [infra_data: any, metrics: record]: nothing -> list<string> {
|
||||
def generate_infrastructure_insights [infra_data: any, metrics: record] {
|
||||
mut insights = []
|
||||
|
||||
if ($metrics.cpu > 80) {
|
||||
@ -505,7 +505,7 @@ def generate_infrastructure_insights [infra_data: any, metrics: record]: nothing
|
||||
$insights
|
||||
}
|
||||
|
||||
def generate_performance_insights [perf_data: any]: any -> list<string> {
|
||||
def generate_performance_insights [perf_data: any] {
|
||||
[
|
||||
"📊 Performance analysis completed"
|
||||
"🔍 Bottlenecks identified in database tier"
|
||||
@ -513,7 +513,7 @@ def generate_performance_insights [perf_data: any]: any -> list<string> {
|
||||
]
|
||||
}
|
||||
|
||||
def generate_cost_insights [cost_data: any]: any -> list<string> {
|
||||
def generate_cost_insights [cost_data: any] {
|
||||
[
|
||||
"💰 Cost analysis reveals optimization opportunities"
|
||||
"📉 Potential savings identified in compute resources"
|
||||
@ -521,7 +521,7 @@ def generate_cost_insights [cost_data: any]: any -> list<string> {
|
||||
]
|
||||
}
|
||||
|
||||
def generate_security_insights [security_data: any]: any -> list<string> {
|
||||
def generate_security_insights [security_data: any] {
|
||||
[
|
||||
"🛡️ Security posture assessment completed"
|
||||
"🔍 No critical vulnerabilities detected"
|
||||
@ -529,7 +529,7 @@ def generate_security_insights [security_data: any]: any -> list<string> {
|
||||
]
|
||||
}
|
||||
|
||||
def generate_predictive_insights [prediction_data: any]: any -> list<string> {
|
||||
def generate_predictive_insights [prediction_data: any] {
|
||||
[
|
||||
"🔮 Predictive models trained on historical data"
|
||||
"📈 Trend analysis shows stable resource usage"
|
||||
@ -537,7 +537,7 @@ def generate_predictive_insights [prediction_data: any]: any -> list<string> {
|
||||
]
|
||||
}
|
||||
|
||||
def generate_troubleshooting_insights [troubleshoot_data: any]: any -> list<string> {
|
||||
def generate_troubleshooting_insights [troubleshoot_data: any] {
|
||||
[
|
||||
"🔧 Issue patterns identified"
|
||||
"🎯 Root cause analysis in progress"
|
||||
@ -546,7 +546,7 @@ def generate_troubleshooting_insights [troubleshoot_data: any]: any -> list<stri
|
||||
}
|
||||
|
||||
# Recommendation generation
|
||||
def generate_recommendations [category: string, data: any]: nothing -> list<string> {
|
||||
def generate_recommendations [category: string, data: any] {
|
||||
match $category {
|
||||
"infrastructure" => [
|
||||
"Consider implementing auto-scaling for peak hours"
|
||||
@ -586,7 +586,7 @@ def generate_recommendations [category: string, data: any]: nothing -> list<stri
|
||||
}
|
||||
|
||||
# Response formatting
|
||||
def format_response [result: record, format: string]: nothing -> any {
|
||||
def format_response [result: record, format: string] {
|
||||
match $format {
|
||||
"json" => {
|
||||
$result | to json
|
||||
@ -606,7 +606,7 @@ def format_response [result: record, format: string]: nothing -> any {
|
||||
}
|
||||
}
|
||||
|
||||
def generate_summary [result: record]: record -> string {
|
||||
def generate_summary [result: record] {
|
||||
let insights_text = ($result.insights | str join "\n• ")
|
||||
let recs_text = ($result.recommendations | str join "\n• ")
|
||||
|
||||
@ -633,7 +633,7 @@ export def process_batch_queries [
|
||||
--context: string = "batch"
|
||||
--format: string = "json"
|
||||
--parallel = true
|
||||
]: list<string> -> list<any> {
|
||||
] {
|
||||
|
||||
print $"🔄 Processing batch of ($queries | length) queries..."
|
||||
|
||||
@ -652,7 +652,7 @@ export def process_batch_queries [
|
||||
export def analyze_query_performance [
|
||||
queries: list<string>
|
||||
--iterations: int = 10
|
||||
]: list<string> -> record {
|
||||
] {
|
||||
|
||||
print "📊 Analyzing query performance..."
|
||||
|
||||
@ -687,7 +687,7 @@ export def analyze_query_performance [
|
||||
}
|
||||
|
||||
# Export query capabilities
|
||||
export def get_query_capabilities []: nothing -> record {
|
||||
export def get_query_capabilities [] {
|
||||
{
|
||||
supported_types: $QUERY_TYPES
|
||||
agents: [
|
||||
|
||||
@ -7,7 +7,7 @@ use ../lib_provisioning/utils/settings.nu *
|
||||
use ../main_provisioning/query.nu *
|
||||
|
||||
# Route definitions for the API server
|
||||
export def get_route_definitions []: nothing -> list {
|
||||
export def get_route_definitions [] {
|
||||
[
|
||||
{
|
||||
method: "GET"
|
||||
@ -190,7 +190,7 @@ export def get_route_definitions []: nothing -> list {
|
||||
}
|
||||
|
||||
# Generate OpenAPI/Swagger specification
|
||||
export def generate_api_spec []: nothing -> record {
|
||||
export def generate_api_spec [] {
|
||||
let routes = get_route_definitions
|
||||
|
||||
{
|
||||
@ -226,7 +226,7 @@ export def generate_api_spec []: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
def generate_paths []: list -> record {
|
||||
def generate_paths [] {
|
||||
let paths = {}
|
||||
|
||||
$in | each { |route|
|
||||
@ -265,7 +265,7 @@ def generate_paths []: list -> record {
|
||||
} | last
|
||||
}
|
||||
|
||||
def generate_schemas []: nothing -> record {
|
||||
def generate_schemas [] {
|
||||
{
|
||||
Error: {
|
||||
type: "object"
|
||||
@ -319,7 +319,7 @@ def generate_schemas []: nothing -> record {
|
||||
}
|
||||
|
||||
# Generate route documentation
|
||||
export def generate_route_docs []: nothing -> str {
|
||||
export def generate_route_docs [] {
|
||||
let routes = get_route_definitions
|
||||
|
||||
let header = "# Provisioning API Routes\n\nThis document describes all available API endpoints.\n\n"
|
||||
@ -342,7 +342,7 @@ export def generate_route_docs []: nothing -> str {
|
||||
}
|
||||
|
||||
# Validate route configuration
|
||||
export def validate_routes []: nothing -> record {
|
||||
export def validate_routes [] {
|
||||
let routes = get_route_definitions
|
||||
let validation_results = []
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ export def start_api_server [
|
||||
--enable-websocket
|
||||
--enable-cors
|
||||
--debug
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"🚀 Starting Provisioning API Server on ($host):($port)"
|
||||
|
||||
if $debug {
|
||||
@ -56,7 +56,7 @@ export def start_api_server [
|
||||
start_http_server $server_config
|
||||
}
|
||||
|
||||
def check_port_available [port: int]: nothing -> bool {
|
||||
def check_port_available [port: int] {
|
||||
# Try to connect to check if port is in use
|
||||
# If connection succeeds, port is in use; if it fails, port is available
|
||||
let result = (do { http get $"http://127.0.0.1:($port)" } | complete)
|
||||
@ -66,7 +66,7 @@ def check_port_available [port: int]: nothing -> bool {
|
||||
$result.exit_code != 0
|
||||
}
|
||||
|
||||
def get_api_routes []: nothing -> list {
|
||||
def get_api_routes [] {
|
||||
[
|
||||
{ method: "GET", path: "/api/v1/health", handler: "handle_health" }
|
||||
{ method: "GET", path: "/api/v1/query", handler: "handle_query_get" }
|
||||
@ -79,7 +79,7 @@ def get_api_routes []: nothing -> list {
|
||||
]
|
||||
}
|
||||
|
||||
def start_http_server [config: record]: nothing -> nothing {
|
||||
def start_http_server [config: record] {
|
||||
print $"🌐 Starting HTTP server on ($config.host):($config.port)..."
|
||||
|
||||
# Use a Python-based HTTP server for better compatibility
|
||||
@ -96,7 +96,7 @@ def start_http_server [config: record]: nothing -> nothing {
|
||||
python3 $temp_server
|
||||
}
|
||||
|
||||
def create_python_server [config: record]: nothing -> str {
|
||||
def create_python_server [config: record] {
|
||||
let cors_headers = if $config.enable_cors {
|
||||
'''
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
@ -416,7 +416,7 @@ if __name__ == '__main__':
|
||||
export def start_websocket_server [
|
||||
--port: int = 8081
|
||||
--host: string = "localhost"
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"🔗 Starting WebSocket server on ($host):($port) for real-time updates"
|
||||
print "This feature requires additional WebSocket implementation"
|
||||
print "Consider using a Rust-based WebSocket server for production use"
|
||||
@ -426,7 +426,7 @@ export def start_websocket_server [
|
||||
export def check_api_health [
|
||||
--host: string = "localhost"
|
||||
--port: int = 8080
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do { http get $"http://($host):($port)/api/v1/health" } | complete)
|
||||
if $result.exit_code != 0 {
|
||||
{
|
||||
|
||||
@ -10,7 +10,7 @@ export def "break-glass request" [
|
||||
--permissions: list<string> = [] # Requested permissions
|
||||
--duration: duration = 4hr # Maximum session duration
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> record {
|
||||
] {
|
||||
if ($justification | is-empty) {
|
||||
error make {msg: "Justification is required for break-glass requests"}
|
||||
}
|
||||
@ -67,7 +67,7 @@ export def "break-glass approve" [
|
||||
request_id: string # Request ID to approve
|
||||
--reason: string = "Approved" # Approval reason
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Get current user info
|
||||
let approver = {
|
||||
id: (whoami)
|
||||
@ -107,7 +107,7 @@ export def "break-glass deny" [
|
||||
request_id: string # Request ID to deny
|
||||
--reason: string = "Denied" # Denial reason
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
# Get current user info
|
||||
let denier = {
|
||||
id: (whoami)
|
||||
@ -133,7 +133,7 @@ export def "break-glass deny" [
|
||||
export def "break-glass activate" [
|
||||
request_id: string # Request ID to activate
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print $"🔓 Activating emergency session for request ($request_id)..."
|
||||
|
||||
let token = (http post $"($orchestrator)/api/v1/break-glass/requests/($request_id)/activate" {})
|
||||
@ -157,7 +157,7 @@ export def "break-glass revoke" [
|
||||
session_id: string # Session ID to revoke
|
||||
--reason: string = "Manual revocation" # Revocation reason
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let payload = {
|
||||
reason: $reason
|
||||
}
|
||||
@ -173,7 +173,7 @@ export def "break-glass revoke" [
|
||||
export def "break-glass list-requests" [
|
||||
--status: string = "pending" # Filter by status (pending, all)
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> table {
|
||||
] {
|
||||
let pending_only = ($status == "pending")
|
||||
|
||||
print $"📋 Listing break-glass requests..."
|
||||
@ -192,7 +192,7 @@ export def "break-glass list-requests" [
|
||||
export def "break-glass list-sessions" [
|
||||
--active-only: bool = false # Show only active sessions
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> table {
|
||||
] {
|
||||
print $"📋 Listing break-glass sessions..."
|
||||
|
||||
let sessions = (http get $"($orchestrator)/api/v1/break-glass/sessions?active_only=($active_only)")
|
||||
@ -209,7 +209,7 @@ export def "break-glass list-sessions" [
|
||||
export def "break-glass show" [
|
||||
session_id: string # Session ID to show
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print $"🔍 Fetching session details for ($session_id)..."
|
||||
|
||||
let session = (http get $"($orchestrator)/api/v1/break-glass/sessions/($session_id)")
|
||||
@ -239,7 +239,7 @@ export def "break-glass audit" [
|
||||
--to: datetime # End time
|
||||
--session-id: string # Filter by session ID
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> table {
|
||||
] {
|
||||
print $"📜 Querying break-glass audit logs..."
|
||||
|
||||
mut params = []
|
||||
@ -271,7 +271,7 @@ export def "break-glass audit" [
|
||||
# Show break-glass statistics
|
||||
export def "break-glass stats" [
|
||||
--orchestrator: string = "http://localhost:8080" # Orchestrator URL
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print $"📊 Fetching break-glass statistics..."
|
||||
|
||||
let stats = (http get $"($orchestrator)/api/v1/break-glass/statistics")
|
||||
@ -299,7 +299,7 @@ export def "break-glass stats" [
|
||||
}
|
||||
|
||||
# Break-glass help
|
||||
export def "break-glass help" []: nothing -> nothing {
|
||||
export def "break-glass help" [] {
|
||||
print "Break-Glass Emergency Access System"
|
||||
print ""
|
||||
print "Commands:"
|
||||
|
||||
@ -23,7 +23,7 @@ export def "main create" [
|
||||
--notitles # not tittles
|
||||
--helpinfo (-h) # For more details use options "help" (no dashes)
|
||||
--out: string # Print Output format: json, yaml, text (default)
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if ($out | is-not-empty) {
|
||||
$env.PROVISIONING_OUT = $out
|
||||
$env.PROVISIONING_NO_TERMINAL = true
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
use ../lib_provisioning/config/accessor.nu config-get
|
||||
|
||||
# Discover all available clusters
|
||||
export def discover-clusters []: nothing -> list<record> {
|
||||
export def discover-clusters [] {
|
||||
# Get absolute path to extensions directory from config
|
||||
let clusters_path = (config-get "paths.clusters" | path expand)
|
||||
|
||||
@ -31,7 +31,7 @@ export def discover-clusters []: nothing -> list<record> {
|
||||
}
|
||||
|
||||
# Extract metadata from a cluster's Nickel module
|
||||
def extract_cluster_metadata [name: string, schema_path: string]: nothing -> record {
|
||||
def extract_cluster_metadata [name: string, schema_path: string] {
|
||||
let mod_path = ($schema_path | path join "nickel.mod")
|
||||
let mod_content = (open $mod_path | from toml)
|
||||
|
||||
@ -71,7 +71,7 @@ def extract_cluster_metadata [name: string, schema_path: string]: nothing -> rec
|
||||
}
|
||||
|
||||
# Extract description from Nickel schema file
|
||||
def extract_schema_description [schema_file: string]: nothing -> string {
|
||||
def extract_schema_description [schema_file: string] {
|
||||
if not ($schema_file | path exists) {
|
||||
return ""
|
||||
}
|
||||
@ -91,7 +91,7 @@ def extract_schema_description [schema_file: string]: nothing -> string {
|
||||
}
|
||||
|
||||
# Extract cluster components from schema
|
||||
def extract_cluster_components [schema_file: string]: nothing -> list<string> {
|
||||
def extract_cluster_components [schema_file: string] {
|
||||
if not ($schema_file | path exists) {
|
||||
return []
|
||||
}
|
||||
@ -116,7 +116,7 @@ def extract_cluster_components [schema_file: string]: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Determine cluster type based on components
|
||||
def determine_cluster_type [components: list<string>]: nothing -> string {
|
||||
def determine_cluster_type [components: list<string>] {
|
||||
if ($components | any { |comp| $comp in ["buildkit", "registry", "docker"] }) {
|
||||
"ci-cd"
|
||||
} else if ($components | any { |comp| $comp in ["prometheus", "grafana"] }) {
|
||||
@ -133,7 +133,7 @@ def determine_cluster_type [components: list<string>]: nothing -> string {
|
||||
}
|
||||
|
||||
# Search clusters by name, type, or components
|
||||
export def search-clusters [query: string]: nothing -> list<record> {
|
||||
export def search-clusters [query: string] {
|
||||
discover-clusters
|
||||
| where (
|
||||
($it.name | str contains $query) or
|
||||
@ -144,7 +144,7 @@ export def search-clusters [query: string]: nothing -> list<record> {
|
||||
}
|
||||
|
||||
# Get specific cluster info
|
||||
export def get-cluster-info [name: string]: nothing -> record {
|
||||
export def get-cluster-info [name: string] {
|
||||
let clusters = (discover-clusters)
|
||||
let found = ($clusters | where name == $name | first)
|
||||
|
||||
@ -156,13 +156,13 @@ export def get-cluster-info [name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# List clusters by type
|
||||
export def list-clusters-by-type [type: string]: nothing -> list<record> {
|
||||
export def list-clusters-by-type [type: string] {
|
||||
discover-clusters
|
||||
| where cluster_type == $type
|
||||
}
|
||||
|
||||
# Validate cluster availability
|
||||
export def validate-clusters [names: list<string>]: nothing -> record {
|
||||
export def validate-clusters [names: list<string>] {
|
||||
let available = (discover-clusters | get name)
|
||||
let missing = ($names | where ($it not-in $available))
|
||||
let found = ($names | where ($it in $available))
|
||||
@ -176,13 +176,13 @@ export def validate-clusters [names: list<string>]: nothing -> record {
|
||||
}
|
||||
|
||||
# Get clusters that use specific components
|
||||
export def find-clusters-with-component [component: string]: nothing -> list<record> {
|
||||
export def find-clusters-with-component [component: string] {
|
||||
discover-clusters
|
||||
| where ($it.components | any { |comp| $comp == $component })
|
||||
}
|
||||
|
||||
# List all available cluster types
|
||||
export def list-cluster-types []: nothing -> list<string> {
|
||||
export def list-cluster-types [] {
|
||||
discover-clusters
|
||||
| get cluster_type
|
||||
| uniq
|
||||
|
||||
@ -23,7 +23,7 @@ export def "main generate" [
|
||||
--notitles # not tittles
|
||||
--helpinfo (-h) # For more details use options "help" (no dashes)
|
||||
--out: string # Print Output format: json, yaml, text (default)
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if ($out | is-not-empty) {
|
||||
$env.PROVISIONING_OUT = $out
|
||||
$env.PROVISIONING_NO_TERMINAL = true
|
||||
|
||||
@ -1,122 +1,184 @@
|
||||
use utils.nu servers_selector
|
||||
use utils.nu *
|
||||
use lib_provisioning *
|
||||
use run.nu *
|
||||
use check_mode.nu *
|
||||
use ../lib_provisioning/config/accessor.nu *
|
||||
use ../lib_provisioning/utils/hints.nu *
|
||||
|
||||
#use clusters/run.nu run_cluster
|
||||
#use ../extensions/taskservs/run.nu run_taskserv
|
||||
def install_from_server [
|
||||
defs: record
|
||||
server_cluster_path: string
|
||||
server_taskserv_path: string
|
||||
wk_server: string
|
||||
]: nothing -> bool {
|
||||
_print $"($defs.cluster.name) on ($defs.server.hostname) install (_ansi purple_bold)from ($defs.cluster_install_mode)(_ansi reset)"
|
||||
run_cluster $defs ((get-run-clusters-path) | path join $defs.cluster.name | path join $server_cluster_path)
|
||||
($wk_server | path join $defs.cluster.name)
|
||||
] {
|
||||
_print (
|
||||
$"(_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
|
||||
$"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
|
||||
$"(_ansi purple_bold)from ($defs.taskserv_install_mode)(_ansi reset)"
|
||||
)
|
||||
let run_taskservs_path = (get-run-taskservs-path)
|
||||
(run_taskserv $defs
|
||||
($run_taskservs_path | path join $defs.taskserv.name | path join $server_taskserv_path)
|
||||
($wk_server | path join $defs.taskserv.name)
|
||||
)
|
||||
}
|
||||
def install_from_library [
|
||||
defs: record
|
||||
server_cluster_path: string
|
||||
server_taskserv_path: string
|
||||
wk_server: string
|
||||
]: nothing -> bool {
|
||||
_print $"($defs.cluster.name) on ($defs.server.hostname) installed (_ansi purple_bold)from library(_ansi reset)"
|
||||
run_cluster $defs ((get-clusters-path) |path join $defs.cluster.name | path join $defs.cluster_profile)
|
||||
($wk_server | path join $defs.cluster.name)
|
||||
] {
|
||||
_print (
|
||||
$"(_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
|
||||
$"($defs.server.hostname) (_ansi default_dimmed)install(_ansi reset) " +
|
||||
$"(_ansi purple_bold)from library(_ansi reset)"
|
||||
)
|
||||
let taskservs_path = (get-taskservs-path)
|
||||
( run_taskserv $defs
|
||||
($taskservs_path | path join $defs.taskserv.name | path join $defs.taskserv_profile)
|
||||
($wk_server | path join $defs.taskserv.name)
|
||||
)
|
||||
}
|
||||
|
||||
export def on_clusters [
|
||||
export def on_taskservs [
|
||||
settings: record
|
||||
match_cluster: string
|
||||
match_taskserv: string
|
||||
match_taskserv_profile: string
|
||||
match_server: string
|
||||
iptype: string
|
||||
check: bool
|
||||
]: nothing -> bool {
|
||||
# use ../../../providers/prov_lib/middleware.nu mw_get_ip
|
||||
_print $"Running (_ansi yellow_bold)clusters(_ansi reset) ..."
|
||||
if (get-provisioning-use-sops) == "" {
|
||||
] {
|
||||
_print $"Running (_ansi yellow_bold)taskservs(_ansi reset) ..."
|
||||
let provisioning_sops = ($env.PROVISIONING_SOPS? | default "")
|
||||
if $provisioning_sops == "" {
|
||||
# A SOPS load env
|
||||
$env.CURRENT_INFRA_PATH = $"($settings.infra_path)/($settings.infra)"
|
||||
use sops_env.nu
|
||||
$env.CURRENT_INFRA_PATH = ($settings.infra_path | path join $settings.infra)
|
||||
use ../sops_env.nu
|
||||
}
|
||||
let ip_type = if $iptype == "" { "public" } else { $iptype }
|
||||
mut server_pos = -1
|
||||
mut cluster_pos = -1
|
||||
mut curr_cluster = 0
|
||||
let created_clusters_dirpath = ( $settings.data.created_clusters_dirpath | default "/tmp" |
|
||||
let str_created_taskservs_dirpath = ( $settings.data.created_taskservs_dirpath | default (["/tmp"] | path join) |
|
||||
str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME | str replace "NOW" $env.NOW
|
||||
)
|
||||
let root_wk_server = ($created_clusters_dirpath | path join "on-server")
|
||||
let created_taskservs_dirpath = if ($str_created_taskservs_dirpath | str starts-with "/" ) { $str_created_taskservs_dirpath } else { $settings.src_path | path join $str_created_taskservs_dirpath }
|
||||
let root_wk_server = ($created_taskservs_dirpath | path join "on-server")
|
||||
if not ($root_wk_server | path exists ) { ^mkdir "-p" $root_wk_server }
|
||||
let dflt_clean_created_clusters = ($settings.data.defaults_servers.clean_created_clusters? | default $created_clusters_dirpath |
|
||||
let dflt_clean_created_taskservs = ($settings.data.clean_created_taskservs? | default $created_taskservs_dirpath |
|
||||
str replace "./" $"($settings.src_path)/" | str replace "~" $env.HOME
|
||||
)
|
||||
let run_ops = if (is-debug-enabled) { "bash -x" } else { "" }
|
||||
for srvr in $settings.data.servers {
|
||||
# continue
|
||||
_print $"on (_ansi green_bold)($srvr.hostname)(_ansi reset) ..."
|
||||
$server_pos += 1
|
||||
$cluster_pos = -1
|
||||
_print $"On server ($srvr.hostname) pos ($server_pos) ..."
|
||||
if $match_server != "" and $srvr.hostname != $match_server { continue }
|
||||
let clean_created_clusters = (($settings.data.servers | try { get $server_pos).clean_created_clusters? } catch { $dflt_clean_created_clusters ) }
|
||||
let ip = if (is-debug-check-enabled) {
|
||||
$settings.data.servers
|
||||
| enumerate
|
||||
| where {|it|
|
||||
$match_server == "" or $it.item.hostname == $match_server
|
||||
}
|
||||
| each {|it|
|
||||
let server_pos = $it.index
|
||||
let srvr = $it.item
|
||||
_print $"on (_ansi green_bold)($srvr.hostname)(_ansi reset) pos ($server_pos) ..."
|
||||
let clean_created_taskservs = ($settings.data.servers | try { get $server_pos } catch { | try { get clean_created_taskservs } catch { null } $dflt_clean_created_taskservs ) }
|
||||
|
||||
# Determine IP address
|
||||
let ip = if (is-debug-check-enabled) or $check {
|
||||
"127.0.0.1"
|
||||
} else {
|
||||
let curr_ip = (mw_get_ip $settings $srvr $ip_type false | default "")
|
||||
if $curr_ip == "" {
|
||||
_print $"🛑 No IP ($ip_type) found for (_ansi green_bold)($srvr.hostname)(_ansi reset) ($server_pos) "
|
||||
continue
|
||||
null
|
||||
} else {
|
||||
let network_public_ip = ($srvr | try { get network_public_ip } catch { "") }
|
||||
if ($network_public_ip | is-not-empty) and $network_public_ip != $curr_ip {
|
||||
_print $"🛑 IP ($network_public_ip) not equal to ($curr_ip) in (_ansi green_bold)($srvr.hostname)(_ansi reset)"
|
||||
}
|
||||
#use utils.nu wait_for_server
|
||||
|
||||
# Check if server is in running state
|
||||
if not (wait_for_server $server_pos $srvr $settings $curr_ip) {
|
||||
print $"🛑 server ($srvr.hostname) ($curr_ip) (_ansi red_bold)not in running state(_ansi reset)"
|
||||
continue
|
||||
}
|
||||
_print $"🛑 server ($srvr.hostname) ($curr_ip) (_ansi red_bold)not in running state(_ansi reset)"
|
||||
null
|
||||
} else {
|
||||
$curr_ip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Process server only if we have valid IP
|
||||
if ($ip != null) {
|
||||
let server = ($srvr | merge { ip_addresses: { pub: $ip, priv: $srvr.network_private_ip }})
|
||||
let wk_server = ($root_wk_server | path join $server.hostname)
|
||||
if ($wk_server | path exists ) { rm -rf $wk_server }
|
||||
^mkdir "-p" $wk_server
|
||||
for cluster in $server.clusters {
|
||||
$cluster_pos += 1
|
||||
if $cluster_pos > $curr_cluster { break }
|
||||
$curr_cluster += 1
|
||||
if $match_cluster != "" and $match_cluster != $cluster.name { continue }
|
||||
if not ((get-clusters-path) | path join $cluster.name | path exists) {
|
||||
print $"cluster path: ((get-clusters-path) | path join $cluster.name) (_ansi red_bold)not found(_ansi reset)"
|
||||
continue
|
||||
$server.taskservs
|
||||
| enumerate
|
||||
| where {|it|
|
||||
let taskserv = $it.item
|
||||
let matches_taskserv = ($match_taskserv == "" or $match_taskserv == $taskserv.name)
|
||||
let matches_profile = ($match_taskserv_profile == "" or $match_taskserv_profile == $taskserv.profile)
|
||||
$matches_taskserv and $matches_profile
|
||||
}
|
||||
if not ($wk_server | path join $cluster.name| path exists) { ^mkdir "-p" ($wk_server | path join $cluster.name) }
|
||||
let $cluster_profile = if $cluster.profile == "" { "default" } else { $cluster.profile }
|
||||
let $cluster_install_mode = if $cluster.install_mode == "" { "library" } else { $cluster.install_mode }
|
||||
let server_cluster_path = ($server.hostname | path join $cluster_profile)
|
||||
| each {|it|
|
||||
let taskserv = $it.item
|
||||
let taskserv_pos = $it.index
|
||||
let taskservs_path = (get-taskservs-path)
|
||||
|
||||
# Check if taskserv path exists - skip if not found
|
||||
if not ($taskservs_path | path join $taskserv.name | path exists) {
|
||||
_print $"taskserv path: ($taskservs_path | path join $taskserv.name) (_ansi red_bold)not found(_ansi reset)"
|
||||
} else {
|
||||
# Taskserv path exists, proceed with processing
|
||||
if not ($wk_server | path join $taskserv.name| path exists) { ^mkdir "-p" ($wk_server | path join $taskserv.name) }
|
||||
let $taskserv_profile = if $taskserv.profile == "" { "default" } else { $taskserv.profile }
|
||||
let $taskserv_install_mode = if $taskserv.install_mode == "" { "library" } else { $taskserv.install_mode }
|
||||
let server_taskserv_path = ($server.hostname | path join $taskserv_profile)
|
||||
let defs = {
|
||||
settings: $settings, server: $server, cluster: $cluster,
|
||||
cluster_install_mode: $cluster_install_mode, cluster_profile: $cluster_profile,
|
||||
pos: { server: $"($server_pos)", cluster: $cluster_pos}, ip: $ip }
|
||||
match $cluster.install_mode {
|
||||
settings: $settings, server: $server, taskserv: $taskserv,
|
||||
taskserv_install_mode: $taskserv_install_mode, taskserv_profile: $taskserv_profile,
|
||||
pos: { server: $"($server_pos)", taskserv: $taskserv_pos}, ip: $ip, check: $check }
|
||||
|
||||
# Enhanced check mode
|
||||
if $check {
|
||||
let check_result = (run-check-mode $taskserv.name $taskserv_profile $settings $server --verbose=(is-debug-enabled))
|
||||
if $check_result.overall_valid {
|
||||
# Check passed, proceed (no action needed, validation was successful)
|
||||
} else {
|
||||
_print $"(_ansi red)⊘ Skipping deployment due to validation errors(_ansi reset)"
|
||||
}
|
||||
} else {
|
||||
# Normal installation mode
|
||||
match $taskserv.install_mode {
|
||||
"server" | "getfile" => {
|
||||
(install_from_server $defs $server_cluster_path $wk_server )
|
||||
(install_from_server $defs $server_taskserv_path $wk_server )
|
||||
},
|
||||
"library-server" => {
|
||||
(install_from_library $defs $server_cluster_path $wk_server)
|
||||
(install_from_server $defs $server_cluster_path $wk_server )
|
||||
(install_from_library $defs $server_taskserv_path $wk_server)
|
||||
(install_from_server $defs $server_taskserv_path $wk_server )
|
||||
},
|
||||
"server-library" => {
|
||||
(install_from_server $defs $server_cluster_path $wk_server )
|
||||
(install_from_library $defs $server_cluster_path $wk_server)
|
||||
(install_from_server $defs $server_taskserv_path $wk_server )
|
||||
(install_from_library $defs $server_taskserv_path $wk_server)
|
||||
},
|
||||
"library" => {
|
||||
(install_from_library $defs $server_cluster_path $wk_server)
|
||||
(install_from_library $defs $server_taskserv_path $wk_server)
|
||||
},
|
||||
}
|
||||
if $clean_created_clusters == "yes" { rm -rf ($wk_server | pth join $cluster.name) }
|
||||
}
|
||||
if $clean_created_clusters == "yes" { rm -rf $wk_server }
|
||||
print $"Clusters completed on ($server.hostname)"
|
||||
if $clean_created_taskservs == "yes" { rm -rf ($wk_server | pth join $taskserv.name) }
|
||||
}
|
||||
}
|
||||
if $clean_created_taskservs == "yes" { rm -rf $wk_server }
|
||||
_print $"Tasks completed on ($server.hostname)"
|
||||
}
|
||||
}
|
||||
if ("/tmp/k8s_join.sh" | path exists) { cp "/tmp/k8s_join.sh" $root_wk_server ; rm -r /tmp/k8s_join.sh }
|
||||
if $dflt_clean_created_clusters == "yes" { rm -rf $root_wk_server }
|
||||
print $"✅ Clusters (_ansi green_bold)completed(_ansi reset) ....."
|
||||
if $dflt_clean_created_taskservs == "yes" { rm -rf $root_wk_server }
|
||||
_print $"✅ Tasks (_ansi green_bold)completed(_ansi reset) ($match_server) ($match_taskserv) ($match_taskserv_profile) ....."
|
||||
if not $check and ($match_server | is-empty) {
|
||||
#use utils.nu servers_selector
|
||||
servers_selector $settings $ip_type false
|
||||
}
|
||||
|
||||
# Show next-step hints after successful taskserv installation
|
||||
if not $check and ($match_taskserv | is-not-empty) {
|
||||
show-next-step "taskserv_create" {name: $match_taskserv}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export def load-clusters [
|
||||
clusters: list<string>,
|
||||
--force = false # Overwrite existing
|
||||
--level: string = "auto" # "workspace", "infra", or "auto"
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Determine target layer
|
||||
let layer_info = (determine-layer --workspace $target_path --infra $target_path --level $level)
|
||||
let load_path = $layer_info.path
|
||||
@ -55,7 +55,7 @@ export def load-clusters [
|
||||
}
|
||||
|
||||
# Load a single cluster
|
||||
def load-single-cluster [target_path: string, name: string, force: bool, layer: string]: nothing -> record {
|
||||
def load-single-cluster [target_path: string, name: string, force: bool, layer: string] {
|
||||
let result = (do {
|
||||
let cluster_info = (get-cluster-info $name)
|
||||
let target_dir = ($target_path | path join ".clusters" $name)
|
||||
@ -181,7 +181,7 @@ def update-clusters-manifest [target_path: string, clusters: list<string>, layer
|
||||
}
|
||||
|
||||
# Remove cluster from workspace
|
||||
export def unload-cluster [workspace: string, name: string]: nothing -> record {
|
||||
export def unload-cluster [workspace: string, name: string] {
|
||||
let target_dir = ($workspace | path join ".clusters" $name)
|
||||
|
||||
if not ($target_dir | path exists) {
|
||||
@ -220,7 +220,7 @@ export def unload-cluster [workspace: string, name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# List loaded clusters in workspace
|
||||
export def list-loaded-clusters [workspace: string]: nothing -> list<record> {
|
||||
export def list-loaded-clusters [workspace: string] {
|
||||
let manifest_path = ($workspace | path join "clusters.manifest.yaml")
|
||||
|
||||
if not ($manifest_path | path exists) {
|
||||
@ -236,7 +236,7 @@ export def clone-cluster [
|
||||
workspace: string,
|
||||
source_name: string,
|
||||
target_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Check if source cluster is loaded
|
||||
let loaded = (list-loaded-clusters $workspace)
|
||||
let source_loaded = ($loaded | where name == $source_name | length) > 0
|
||||
|
||||
@ -2,7 +2,7 @@ use ../lib_provisioning/config/accessor.nu *
|
||||
|
||||
export def provisioning_options [
|
||||
source: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let provisioning_name = (get-provisioning-name)
|
||||
let provisioning_path = (get-base-path)
|
||||
let provisioning_url = (get-provisioning-url)
|
||||
|
||||
@ -1,19 +1,24 @@
|
||||
#use utils.nu cluster_get_file
|
||||
#use utils/templates.nu on_template_path
|
||||
|
||||
use std
|
||||
use ../lib_provisioning/config/accessor.nu [is-debug-enabled, is-debug-check-enabled]
|
||||
use ../lib_provisioning/config/accessor.nu *
|
||||
#use utils.nu taskserv_get_file
|
||||
#use utils/templates.nu on_template_path
|
||||
|
||||
def make_cmd_env_temp [
|
||||
defs: record
|
||||
cluster_env_path: string
|
||||
taskserv_env_path: string
|
||||
wk_vars: string
|
||||
]: nothing -> string {
|
||||
let cmd_env_temp = $"($cluster_env_path)/cmd_env_(mktemp --tmpdir-path $cluster_env_path --suffix ".sh" | path basename)"
|
||||
# export all 'PROVISIONING_' $env vars to SHELL
|
||||
($"export NU_LOG_LEVEL=($env.NU_LOG_LEVEL)\n" +
|
||||
($env | items {|key, value| if ($key | str starts-with "PROVISIONING_") {echo $'export ($key)="($value)"\n'} } | compact --empty | to text)
|
||||
] {
|
||||
let cmd_env_temp = $"($taskserv_env_path | path join "cmd_env")_(mktemp --tmpdir-path $taskserv_env_path --suffix ".sh" | path basename)"
|
||||
($"export PROVISIONING_VARS=($wk_vars)\nexport PROVISIONING_DEBUG=((is-debug-enabled))\n" +
|
||||
$"export NU_LOG_LEVEL=($env.NU_LOG_LEVEL)\n" +
|
||||
$"export PROVISIONING_RESOURCES=((get-provisioning-resources))\n" +
|
||||
$"export PROVISIONING_SETTINGS_SRC=($defs.settings.src)\nexport PROVISIONING_SETTINGS_SRC_PATH=($defs.settings.src_path)\n" +
|
||||
$"export PROVISIONING_KLOUD=($defs.settings.infra)\nexport PROVISIONING_KLOUD_PATH=($defs.settings.infra_path)\n" +
|
||||
$"export PROVISIONING_USE_SOPS=((get-provisioning-use-sops))\nexport PROVISIONING_WK_ENV_PATH=($taskserv_env_path)\n" +
|
||||
$"export SOPS_AGE_KEY_FILE=($env.SOPS_AGE_KEY_FILE)\nexport PROVISIONING_KAGE=($env.PROVISIONING_KAGE)\n" +
|
||||
$"export SOPS_AGE_RECIPIENTS=($env.SOPS_AGE_RECIPIENTS)\n"
|
||||
) | save --force $cmd_env_temp
|
||||
if (is-debug-enabled) { _print $"cmd_env_temp: ($cmd_env_temp)" }
|
||||
$cmd_env_temp
|
||||
}
|
||||
def run_cmd [
|
||||
@ -21,67 +26,75 @@ def run_cmd [
|
||||
title: string
|
||||
where: string
|
||||
defs: record
|
||||
cluster_env_path: string
|
||||
taskserv_env_path: string
|
||||
wk_vars: string
|
||||
]: nothing -> nothing {
|
||||
_print $"($title) for ($defs.cluster.name) on ($defs.server.hostname) ($defs.pos.server) ..."
|
||||
if $defs.check { return }
|
||||
let runner = (grep "^#!" $"($cluster_env_path)/($cmd_name)" | str trim)
|
||||
] {
|
||||
_print (
|
||||
$"($title) for (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) (_ansi default_dimmed)on(_ansi reset) " +
|
||||
$"($defs.server.hostname) ($defs.pos.server) ..."
|
||||
)
|
||||
let runner = (grep "^#!" ($taskserv_env_path | path join $cmd_name) | str trim)
|
||||
let run_ops = if (is-debug-enabled) { if ($runner | str contains "bash" ) { "-x" } else { "" } } else { "" }
|
||||
let cmd_env_temp = make_cmd_env_temp $defs $cluster_env_path $wk_vars
|
||||
if ($wk_vars | path exists) {
|
||||
let run_res = if ($runner | str ends-with "bash" ) {
|
||||
(^bash -c $"'source ($cmd_env_temp) ; bash ($run_ops) ($cluster_env_path)/($cmd_name) ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)'" | complete)
|
||||
let cmd_run_file = make_cmd_env_temp $defs $taskserv_env_path $wk_vars
|
||||
if ($cmd_run_file | path exists) and ($wk_vars | path exists) {
|
||||
if ($runner | str ends-with "bash" ) {
|
||||
$"($run_ops) ($taskserv_env_path | path join $cmd_name) ($wk_vars) ($defs.pos.server) ($defs.pos.taskserv) (^pwd)" | save --append $cmd_run_file
|
||||
} else if ($runner | str ends-with "nu" ) {
|
||||
(^bash -c $"'source ($cmd_env_temp); ($env.NU) ($env.NU_ARGS) ($cluster_env_path)/($cmd_name)'" | complete)
|
||||
$"($env.NU) ($env.NU_ARGS) ($taskserv_env_path | path join $cmd_name)" | save --append $cmd_run_file
|
||||
} else {
|
||||
(^bash -c $"'source ($cmd_env_temp); ($cluster_env_path)/($cmd_name) ($wk_vars)'" | complete)
|
||||
$"($taskserv_env_path | path join $cmd_name) ($wk_vars)" | save --append $cmd_run_file
|
||||
}
|
||||
rm -f $cmd_env_temp
|
||||
let run_res = (^bash $cmd_run_file | complete)
|
||||
if $run_res.exit_code != 0 {
|
||||
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name)
|
||||
($cluster_env_path)/($cmd_name) with ($wk_vars) ($defs.pos.server) ($defs.pos.cluster) (^pwd)"
|
||||
$run_res.stdout
|
||||
(throw-error $"🛑 Error server ($defs.server.hostname) taskserv ($defs.taskserv.name)
|
||||
($taskserv_env_path)/($cmd_name) with ($wk_vars) ($defs.pos.server) ($defs.pos.taskserv) (^pwd)"
|
||||
$"($run_res.stdout)\n($run_res.stderr)\n"
|
||||
$where --span (metadata $run_res).span)
|
||||
exit 1
|
||||
}
|
||||
if not (is-debug-enabled) { rm -f $"($cluster_env_path)/prepare" }
|
||||
if (is-debug-enabled) {
|
||||
if ($run_res.stdout | is-not-empty) { _print $"($run_res.stdout)" }
|
||||
if ($run_res.stderr | is-not-empty) { _print $"($run_res.stderr)" }
|
||||
} else {
|
||||
rm -f $cmd_run_file
|
||||
rm -f ($taskserv_env_path | path join "prepare")
|
||||
}
|
||||
}
|
||||
export def run_cluster_library [
|
||||
}
|
||||
export def run_taskserv_library [
|
||||
defs: record
|
||||
cluster_path: string
|
||||
cluster_env_path: string
|
||||
taskserv_path: string
|
||||
taskserv_env_path: string
|
||||
wk_vars: string
|
||||
]: nothing -> bool {
|
||||
if not ($cluster_path | path exists) { return false }
|
||||
] {
|
||||
|
||||
if not ($taskserv_path | path exists) { return false }
|
||||
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
|
||||
let cluster_server_name = $defs.server.hostname
|
||||
rm -rf ($cluster_env_path | path join "*.ncl") ($cluster_env_path | path join "nickel")
|
||||
mkdir ($cluster_env_path | path join "nickel")
|
||||
let taskserv_server_name = $defs.server.hostname
|
||||
rm -rf ...(glob ($taskserv_env_path | path join "*.ncl")) ($taskserv_env_path |path join "nickel")
|
||||
mkdir ($taskserv_env_path | path join "nickel")
|
||||
|
||||
let err_out = ($cluster_env_path | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".err") | path basename)
|
||||
let nickel_temp = ($cluster_env_path | path join "nickel" | path join (mktemp --tmpdir-path $cluster_env_path --suffix ".ncl" ) | path basename)
|
||||
let err_out = ($taskserv_env_path | path join (mktemp --tmpdir-path $taskserv_env_path --suffix ".err" | 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 $env.PROVISIONING_WK_FORMAT == "json" { "json" } else { "yaml" }
|
||||
let wk_data = { defs: $defs.settings.data, pos: $defs.pos, server: $defs.server }
|
||||
let wk_format = if (get-provisioning-wk-format) == "json" { "json" } else { "yaml" }
|
||||
let wk_data = { # providers: $defs.settings.providers,
|
||||
defs: $defs.settings.data,
|
||||
pos: $defs.pos,
|
||||
server: $defs.server
|
||||
}
|
||||
if $wk_format == "json" {
|
||||
$wk_data | to json | save --force $wk_vars
|
||||
} else {
|
||||
$wk_data | to yaml | save --force $wk_vars
|
||||
}
|
||||
if $env.PROVISIONING_USE_nickel {
|
||||
if (get-use-nickel) {
|
||||
cd ($defs.settings.infra_path | path join $defs.settings.infra)
|
||||
let nickel_cluster_path = if ($cluster_path | path join "nickel"| path join $"($defs.cluster.name).ncl" | path exists) {
|
||||
($cluster_path | path join "nickel"| path join $"($defs.cluster.name).ncl")
|
||||
} else if (($cluster_path | path dirname) | path join "nickel"| path join $"($defs.cluster.name).ncl" | path exists) {
|
||||
(($cluster_path | path dirname) | path join "nickel"| path join $"($defs.cluster.name).ncl")
|
||||
} else { "" }
|
||||
if ($nickel_temp | path exists) { rm -f $nickel_temp }
|
||||
let res = (^nickel import -m $wk_format $wk_vars -o $nickel_temp | complete)
|
||||
if $res.exit_code != 0 {
|
||||
print $"❗Nickel import (_ansi red_bold)($wk_vars)(_ansi reset) Errors found "
|
||||
print $res.stdout
|
||||
_print $"❗Nickel import (_ansi red_bold)($wk_vars)(_ansi reset) Errors found "
|
||||
_print $res.stdout
|
||||
rm -f $nickel_temp
|
||||
cd $env.PWD
|
||||
return false
|
||||
@ -89,107 +102,142 @@ export def run_cluster_library [
|
||||
# Very important! Remove external block for import and re-format it
|
||||
# ^sed -i "s/^{//;s/^}//" $nickel_temp
|
||||
open $nickel_temp -r | lines | find -v --regex "^{" | find -v --regex "^}" | save -f $nickel_temp
|
||||
^nickel fmt $nickel_temp
|
||||
if $nickel_cluster_path != "" and ($nickel_cluster_path | path exists) { cat $nickel_cluster_path | save --append $nickel_temp }
|
||||
# } else { print $"❗ No cluster nickel ($defs.cluster.ncl) path found " ; return false }
|
||||
if $env.PROVISIONING_KEYS_PATH != "" {
|
||||
let res = (^nickel fmt $nickel_temp | complete)
|
||||
let nickel_taskserv_path = if ($taskserv_path | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
|
||||
($taskserv_path | path join "nickel"| path join $"($defs.taskserv.name).ncl")
|
||||
} else if ($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
|
||||
($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.name).ncl")
|
||||
} else if ($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.name).ncl" | path exists) {
|
||||
($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.name).ncl")
|
||||
} else { "" }
|
||||
if $nickel_taskserv_path != "" and ($nickel_taskserv_path | path exists) {
|
||||
if (is-debug-enabled) {
|
||||
_print $"adding task name: ($defs.taskserv.name) -> ($nickel_taskserv_path)"
|
||||
}
|
||||
cat $nickel_taskserv_path | save --append $nickel_temp
|
||||
}
|
||||
let nickel_taskserv_profile_path = if ($taskserv_path | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
|
||||
($taskserv_path | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
|
||||
} else if ($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
|
||||
($taskserv_path | path dirname | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
|
||||
} else if ($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.profile).ncl" | path exists) {
|
||||
($taskserv_path | path dirname | path join "default" | path join "nickel"| path join $"($defs.taskserv.profile).ncl")
|
||||
} else { "" }
|
||||
if $nickel_taskserv_profile_path != "" and ($nickel_taskserv_profile_path | path exists) {
|
||||
if (is-debug-enabled) {
|
||||
_print $"adding task profile: ($defs.taskserv.profile) -> ($nickel_taskserv_profile_path)"
|
||||
}
|
||||
cat $nickel_taskserv_profile_path | save --append $nickel_temp
|
||||
}
|
||||
let keys_path_config = (get-keys-path)
|
||||
if $keys_path_config != "" {
|
||||
#use sops on_sops
|
||||
let keys_path = ($defs.settings.src_path | path join $env.PROVISIONING_KEYS_PATH)
|
||||
let keys_path = ($defs.settings.src_path | path join $keys_path_config)
|
||||
if not ($keys_path | path exists) {
|
||||
if (is-debug-enabled) {
|
||||
print $"❗Error KEYS_PATH (_ansi red_bold)($keys_path)(_ansi reset) found "
|
||||
_print $"❗Error KEYS_PATH (_ansi red_bold)($keys_path)(_ansi reset) found "
|
||||
} else {
|
||||
print $"❗Error (_ansi red_bold)KEYS_PATH(_ansi reset) not found "
|
||||
_print $"❗Error (_ansi red_bold)KEYS_PATH(_ansi reset) not found "
|
||||
}
|
||||
return false
|
||||
}
|
||||
(on_sops d $keys_path) | save --append $nickel_temp
|
||||
if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.server.hostname | path join $"($defs.cluster.name).ncl" | path exists ) {
|
||||
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.server.hostname| path join $"($defs.cluster.name).ncl" ) | save --append $nickel_temp
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).ncl" | path exists ) {
|
||||
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $defs.pos.server | path join $"($defs.cluster.name).ncl" ) | save --append $nickel_temp
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).ncl" | path exists ) {
|
||||
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).ncl" ) | save --append $nickel_temp
|
||||
let nickel_defined_taskserv_path = if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl")
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.profile).ncl")
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $"($defs.taskserv.profile).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $"($defs.taskserv.profile).ncl")
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.name).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $"($defs.taskserv.name).ncl")
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $defs.taskserv.profile | path join $"($defs.taskserv.name).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs" | path join $defs.server.hostname | path join $defs.taskserv.profile | path join $"($defs.taskserv.name).ncl")
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).ncl" | path exists ) {
|
||||
($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).ncl")
|
||||
} else { "" }
|
||||
if $nickel_defined_taskserv_path != "" and ($nickel_defined_taskserv_path | path exists) {
|
||||
if (is-debug-enabled) {
|
||||
_print $"adding defs taskserv: ($nickel_defined_taskserv_path)"
|
||||
}
|
||||
cat $nickel_defined_taskserv_path | save --append $nickel_temp
|
||||
}
|
||||
let res = (^nickel $nickel_temp -o $wk_vars | complete)
|
||||
if $res.exit_code != 0 {
|
||||
print $"❗Nickel errors (_ansi red_bold)($nickel_temp)(_ansi reset) found "
|
||||
print $res.stdout
|
||||
_print $"❗Nickel errors (_ansi red_bold)($nickel_temp)(_ansi reset) found "
|
||||
_print $res.stdout
|
||||
_print $res.stderr
|
||||
rm -f $wk_vars
|
||||
cd $env.PWD
|
||||
return false
|
||||
}
|
||||
rm -f $nickel_temp $err_out
|
||||
} else if ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" | path exists) {
|
||||
cat ($defs.settings.src_path | path join "extensions" | path join "clusters" | path join $"($defs.cluster.name).yaml" ) | tee { save -a $wk_vars } | ignore
|
||||
} else if ( $defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).yaml" | path exists) {
|
||||
cat ($defs.settings.src_path | path join "extensions" | path join "taskservs"| path join $"($defs.taskserv.name).yaml") | tee { save -a $wk_vars } | ignore
|
||||
}
|
||||
cd $env.PWD
|
||||
}
|
||||
(^sed -i $"s/NOW/($env.NOW)/g" $wk_vars)
|
||||
if $defs.cluster_install_mode == "library" {
|
||||
let cluster_data = (open $wk_vars)
|
||||
let verbose = if (is-debug-enabled) { true } else { false }
|
||||
if $cluster_data.cluster.copy_paths? != null {
|
||||
if $defs.taskserv_install_mode == "library" {
|
||||
let taskserv_data = (open $wk_vars)
|
||||
let quiet = if (is-debug-enabled) { false } else { true }
|
||||
if $taskserv_data.taskserv? != null and $taskserv_data.taskserv.copy_paths? != null {
|
||||
#use utils/files.nu *
|
||||
for it in $cluster_data.cluster.copy_paths {
|
||||
for it in $taskserv_data.taskserv.copy_paths {
|
||||
let it_list = ($it | split row "|" | default [])
|
||||
let cp_source = ($it_list | try { get 0 } catch { "") }
|
||||
let cp_target = ($it_list | try { get 1 } catch { "") }
|
||||
if ($cp_source | path exists) {
|
||||
copy_prov_files $cp_source ($defs.settings.infra_path | path join $defs.settings.infra) $"($cluster_env_path)/($cp_target)" false $verbose
|
||||
copy_prov_files $cp_source "." ($taskserv_env_path | path join $cp_target) false $quiet
|
||||
} else if ($prov_resources_path | path join $cp_source | path exists) {
|
||||
copy_prov_files $prov_resources_path $cp_source ($taskserv_env_path | path join $cp_target) false $quiet
|
||||
} else if ($"($prov_resources_path)/($cp_source)" | path exists) {
|
||||
copy_prov_files $prov_resources_path $cp_source $"($cluster_env_path)/($cp_target)" false $verbose
|
||||
} else if ($cp_source | file exists) {
|
||||
copy_prov_file $cp_source $"($cluster_env_path)/($cp_target)" $verbose
|
||||
} else if ($"($prov_resources_path)/($cp_source)" | path exists) {
|
||||
copy_prov_file $"($prov_resources_path)/($cp_source)" $"($cluster_env_path)/($cp_target)" $verbose
|
||||
copy_prov_file ($prov_resources_path | path join $cp_source) ($taskserv_env_path | path join $cp_target) $quiet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rm -f ($cluster_env_path | path join "nickel") ($cluster_env_path | path join "*.ncl")
|
||||
on_template_path $cluster_env_path $wk_vars true true
|
||||
if ($cluster_env_path | path join $"env-($defs.cluster.name)" | path exists) {
|
||||
^sed -i 's,\t,,g;s,^ ,,g;/^$/d' ($cluster_env_path | path join $"env-($defs.cluster.name)")
|
||||
rm -f ($taskserv_env_path | path join "nickel") ...(glob $"($taskserv_env_path)/*.ncl")
|
||||
on_template_path $taskserv_env_path $wk_vars true true
|
||||
if ($taskserv_env_path | path join $"env-($defs.taskserv.name)" | path exists) {
|
||||
^sed -i 's,\t,,g;s,^ ,,g;/^$/d' ($taskserv_env_path | path join $"env-($defs.taskserv.name)")
|
||||
}
|
||||
if ($cluster_env_path | path join "prepare" | path exists) {
|
||||
run_cmd "prepare" "Prepare" "run_cluster_library" $defs $cluster_env_path $wk_vars
|
||||
if ($cluster_env_path | path join "resources" | path exists) {
|
||||
on_template_path ($cluster_env_path | path join "resources") $wk_vars false true
|
||||
if ($taskserv_env_path | path join "prepare" | path exists) {
|
||||
run_cmd "prepare" "prepare" "run_taskserv_library" $defs $taskserv_env_path $wk_vars
|
||||
if ($taskserv_env_path | path join "resources" | path exists) {
|
||||
on_template_path ($taskserv_env_path | path join "resources") $wk_vars false true
|
||||
}
|
||||
}
|
||||
if not (is-debug-enabled) {
|
||||
rm -f ($cluster_env_path | path join "*.j2") $err_out $nickel_temp
|
||||
rm -f ...(glob $"($taskserv_env_path)/*.j2") $err_out $nickel_temp
|
||||
}
|
||||
true
|
||||
}
|
||||
export def run_cluster [
|
||||
export def run_taskserv [
|
||||
defs: record
|
||||
cluster_path: string
|
||||
taskserv_path: string
|
||||
env_path: string
|
||||
]: nothing -> bool {
|
||||
if not ($cluster_path | path exists) { return false }
|
||||
if $defs.check { return }
|
||||
] {
|
||||
if not ($taskserv_path | path exists) { return false }
|
||||
let prov_resources_path = ($defs.settings.data.prov_resources_path | default "" | str replace "~" $env.HOME)
|
||||
let created_clusters_dirpath = ($defs.settings.data.created_clusters_dirpath | default "/tmp" |
|
||||
let taskserv_server_name = $defs.server.hostname
|
||||
|
||||
let str_created_taskservs_dirpath = ($defs.settings.data.created_taskservs_dirpath | default "/tmp" |
|
||||
str replace "~" $env.HOME | str replace "NOW" $env.NOW | str replace "./" $"($defs.settings.src_path)/")
|
||||
let cluster_server_name = $defs.server.hostname
|
||||
let created_taskservs_dirpath = if ($str_created_taskservs_dirpath | str starts-with "/" ) { $str_created_taskservs_dirpath } else { $defs.settings.src_path | path join $str_created_taskservs_dirpath }
|
||||
if not ( $created_taskservs_dirpath | path exists) { ^mkdir -p $created_taskservs_dirpath }
|
||||
|
||||
let cluster_env_path = if $defs.cluster_install_mode == "server" { $"($env_path)_($defs.cluster_install_mode)" } else { $env_path }
|
||||
let str_taskserv_env_path = if $defs.taskserv_install_mode == "server" { $"($env_path)_($defs.taskserv_install_mode)" } else { $env_path }
|
||||
let taskserv_env_path = if ($str_taskserv_env_path | str starts-with "/" ) { $str_taskserv_env_path } else { $defs.settings.src_path | path join $str_taskserv_env_path }
|
||||
if not ( $taskserv_env_path | path exists) { ^mkdir -p $taskserv_env_path }
|
||||
|
||||
if not ( $cluster_env_path | path exists) { ^mkdir -p $cluster_env_path }
|
||||
if not ( $created_clusters_dirpath | path exists) { ^mkdir -p $created_clusters_dirpath }
|
||||
(^cp -pr ...(glob ($taskserv_path | path join "*")) $taskserv_env_path)
|
||||
rm -rf ...(glob ($taskserv_env_path | path join "*.ncl")) ($taskserv_env_path | path join "nickel")
|
||||
|
||||
(^cp -pr $"($cluster_path)/*" $cluster_env_path)
|
||||
rm -rf $"($cluster_env_path)/*.ncl" $"($cluster_env_path)/nickel"
|
||||
let wk_vars = ($created_taskservs_dirpath | path join $"($defs.server.hostname).yaml")
|
||||
let require_j2 = (^ls ...(glob ($taskserv_env_path | path join "*.j2")) err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }))
|
||||
|
||||
let wk_vars = $"($created_clusters_dirpath)/($defs.server.hostname).yaml"
|
||||
# if $defs.cluster.name == "kubernetes" and ("/tmp/k8s_join.sh" | path exists) { cp -pr "/tmp/k8s_join.sh" $cluster_env_path }
|
||||
let require_j2 = (^ls ($cluster_env_path | path join "*.j2") err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }))
|
||||
|
||||
|
||||
let res = if $defs.cluster_install_mode == "library" or $require_j2 != "" {
|
||||
(run_cluster_library $defs $cluster_path $cluster_env_path $wk_vars)
|
||||
let res = if $defs.taskserv_install_mode == "library" or $require_j2 != "" {
|
||||
(run_taskserv_library $defs $taskserv_path $taskserv_env_path $wk_vars)
|
||||
}
|
||||
if not $res {
|
||||
if not (is-debug-enabled) { rm -f $wk_vars }
|
||||
@ -199,86 +247,86 @@ export def run_cluster [
|
||||
let tar_ops = if (is-debug-enabled) { "v" } else { "" }
|
||||
let bash_ops = if (is-debug-enabled) { "bash -x" } else { "" }
|
||||
|
||||
let res_tar = (^tar -C $cluster_env_path $"-c($tar_ops)zf" $"/tmp/($defs.cluster.name).tar.gz" . | complete)
|
||||
let res_tar = (^tar -C $taskserv_env_path $"-c($tar_ops)zmf" (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join) . | complete)
|
||||
if $res_tar.exit_code != 0 {
|
||||
_print (
|
||||
$"🛑 Error (_ansi red_bold)tar cluster(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset)" +
|
||||
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) ($cluster_env_path) -> /tmp/($defs.cluster.name).tar.gz"
|
||||
$"🛑 Error (_ansi red_bold)tar taskserv(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset)" +
|
||||
$" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) ($taskserv_env_path) -> (['/tmp' $'($defs.taskserv.name).tar.gz'] | path join)"
|
||||
)
|
||||
_print $res_tar.stdout
|
||||
return false
|
||||
}
|
||||
if $defs.check {
|
||||
if not (is-debug-enabled) {
|
||||
rm -f $wk_vars
|
||||
rm -f $err_out
|
||||
rm -rf $"($cluster_env_path)/*.ncl" $"($cluster_env_path)/nickel"
|
||||
if $err_out != "" { rm -f $err_out }
|
||||
rm -rf ...(glob $"($taskserv_env_path)/*.ncl") ($taskserv_env_path | path join join "nickel")
|
||||
}
|
||||
return true
|
||||
}
|
||||
let is_local = (^ip addr | grep "inet " | grep "$defs.ip")
|
||||
if $is_local != "" and not (is-debug-check-enabled) {
|
||||
if $defs.cluster_install_mode == "getfile" {
|
||||
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true true) { return false }
|
||||
if $defs.taskserv_install_mode == "getfile" {
|
||||
if (taskserv_get_file $defs.settings $defs.taskserv $defs.server $defs.ip true true) { return false }
|
||||
return true
|
||||
}
|
||||
rm -rf $"/tmp/($defs.cluster.name)"
|
||||
mkdir $"/tmp/($defs.cluster.name)"
|
||||
cd $"/tmp/($defs.cluster.name)"
|
||||
tar x($tar_ops)zf $"/tmp/($defs.cluster.name).tar.gz"
|
||||
let res_run = (^sudo $bash_ops $"./install-($defs.cluster.name).sh" err> $err_out | complete)
|
||||
rm -rf (["/tmp" $defs.taskserv.name ] | path join)
|
||||
mkdir (["/tmp" $defs.taskserv.name ] | path join)
|
||||
cd (["/tmp" $defs.taskserv.name ] | path join)
|
||||
tar x($tar_ops)zmf (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join)
|
||||
let res_run = (^sudo $bash_ops $"./install-($defs.taskserv.name).sh" err> $err_out | complete)
|
||||
if $res_run.exit_code != 0 {
|
||||
(throw-error $"🛑 Error server ($defs.server.hostname) cluster ($defs.cluster.name)
|
||||
./install-($defs.cluster.name).sh ($defs.server_pos) ($defs.cluster_pos) (^pwd)"
|
||||
(throw-error $"🛑 Error server ($defs.server.hostname) taskserv ($defs.taskserv.name)
|
||||
./install-($defs.taskserv.name).sh ($defs.server_pos) ($defs.taskserv_pos) (^pwd)"
|
||||
$"($res_run.stdout)\n(cat $err_out)"
|
||||
"run_cluster_library" --span (metadata $res_run).span)
|
||||
"run_taskserv_library" --span (metadata $res_run).span)
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
rm -fr $"/tmp/($defs.cluster.name).tar.gz" $"/tmp/($defs.cluster.name)"
|
||||
rm -fr (["/tmp" $"($defs.taskserv.name).tar.gz"] | path join) (["/tmp" $"($defs.taskserv.name)"] | path join)
|
||||
} else {
|
||||
if $defs.cluster_install_mode == "getfile" {
|
||||
if (cluster_get_file $defs.settings $defs.cluster $defs.server $defs.ip true false) { return false }
|
||||
if $defs.taskserv_install_mode == "getfile" {
|
||||
if (taskserv_get_file $defs.settings $defs.taskserv $defs.server $defs.ip true false) { return false }
|
||||
return true
|
||||
}
|
||||
if not (is-debug-check-enabled) {
|
||||
#use ssh.nu *
|
||||
let scp_list: list<string> = ([] | append $"/tmp/($defs.cluster.name).tar.gz")
|
||||
let scp_list: list<string> = ([] | append $"/tmp/($defs.taskserv.name).tar.gz")
|
||||
if not (scp_to $defs.settings $defs.server $scp_list "/tmp" $defs.ip) {
|
||||
_print (
|
||||
$"🛑 Error (_ansi red_bold)ssh_cp(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
|
||||
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) /tmp/($defs.cluster.name).tar.gz"
|
||||
$"🛑 Error (_ansi red_bold)ssh_to(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
|
||||
$" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) /tmp/($defs.taskserv.name).tar.gz"
|
||||
)
|
||||
return false
|
||||
}
|
||||
# $"rm -rf /tmp/($defs.taskserv.name); mkdir -p /tmp/($defs.taskserv.name) ;" +
|
||||
let run_ops = if (is-debug-enabled) { "bash -x" } else { "" }
|
||||
let cmd = (
|
||||
$"rm -rf /tmp/($defs.cluster.name) ; mkdir /tmp/($defs.cluster.name) ; cd /tmp/($defs.cluster.name) ;" +
|
||||
$" sudo tar x($tar_ops)zf /tmp/($defs.cluster.name).tar.gz;" +
|
||||
$" sudo ($bash_ops) ./install-($defs.cluster.name).sh " # ($env.PROVISIONING_MATCH_CMD) "
|
||||
$"rm -rf /tmp/($defs.taskserv.name); mkdir -p /tmp/($defs.taskserv.name) ;" +
|
||||
$" cd /tmp/($defs.taskserv.name) ; sudo tar x($tar_ops)zmf /tmp/($defs.taskserv.name).tar.gz &&" +
|
||||
$" sudo ($run_ops) ./install-($defs.taskserv.name).sh " # ($env.PROVISIONING_MATCH_CMD) "
|
||||
)
|
||||
if not (ssh_cmd $defs.settings $defs.server true $cmd $defs.ip) {
|
||||
if not (ssh_cmd $defs.settings $defs.server false $cmd $defs.ip) {
|
||||
_print (
|
||||
$"🛑 Error (_ansi red_bold)ssh_cmd(_ansi reset) server (_ansi green_bold)($defs.server.hostname)(_ansi reset) [($defs.ip)] " +
|
||||
$" cluster (_ansi yellow_bold)($defs.cluster.name)(_ansi reset) install_($defs.cluster.name).sh"
|
||||
$" taskserv (_ansi yellow_bold)($defs.taskserv.name)(_ansi reset) install_($defs.taskserv.name).sh"
|
||||
)
|
||||
return false
|
||||
}
|
||||
# if $defs.cluster.name == "kubernetes" { let _res_k8s = (scp_from $defs.settings $defs.server "/tmp/k8s_join.sh" "/tmp" $defs.ip) }
|
||||
if not (is-debug-enabled) {
|
||||
let rm_cmd = $"sudo rm -f /tmp/($defs.cluster.name).tar.gz; sudo rm -rf /tmp/($defs.cluster.name)"
|
||||
let _res = (ssh_cmd $defs.settings $defs.server true $rm_cmd $defs.ip)
|
||||
rm -f $"/tmp/($defs.cluster.name).tar.gz"
|
||||
let rm_cmd = $"sudo rm -f /tmp/($defs.taskserv.name).tar.gz; sudo rm -rf /tmp/($defs.taskserv.name)"
|
||||
let _res = (ssh_cmd $defs.settings $defs.server false $rm_cmd $defs.ip)
|
||||
rm -f $"/tmp/($defs.taskserv.name).tar.gz"
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($"($cluster_path)/postrun" | path exists ) {
|
||||
cp $"($cluster_path)/postrun" $"($cluster_env_path)/postrun"
|
||||
run_cmd "postrun" "PostRune" "run_cluster_library" $defs $cluster_env_path $wk_vars
|
||||
if ($taskserv_path | path join "postrun" | path exists ) {
|
||||
cp ($taskserv_path | path join "postrun") ($taskserv_env_path | path join "postrun")
|
||||
run_cmd "postrun" "PostRune" "run_taskserv_library" $defs $taskserv_env_path $wk_vars
|
||||
}
|
||||
if not (is-debug-enabled) {
|
||||
rm -f $wk_vars
|
||||
rm -f $err_out
|
||||
rm -rf $"($cluster_env_path)/*.ncl" $"($cluster_env_path)/nickel"
|
||||
if $err_out != "" { rm -f $err_out }
|
||||
rm -rf ...(glob $"($taskserv_env_path)/*.ncl") ($taskserv_env_path | path join join "nickel")
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@ -1,61 +1,101 @@
|
||||
# Hetzner Cloud utility functions
|
||||
use env.nu *
|
||||
|
||||
# Parse record or string to server name
|
||||
export def parse_server_identifier [input: any]: nothing -> string {
|
||||
if ($input | describe) == "string" {
|
||||
$input
|
||||
} else if ($input | has hostname) {
|
||||
$input.hostname
|
||||
} else if ($input | has name) {
|
||||
$input.name
|
||||
} else if ($input | has id) {
|
||||
($input.id | into string)
|
||||
} else {
|
||||
($input | into string)
|
||||
}
|
||||
}
|
||||
|
||||
#use ssh.nu *
|
||||
export def cluster_get_file [
|
||||
settings: record
|
||||
cluster: record
|
||||
server: record
|
||||
live_ip: string
|
||||
req_sudo: bool
|
||||
local_mode: bool
|
||||
]: nothing -> bool {
|
||||
let target_path = ($cluster.target_path | default "")
|
||||
if $target_path == "" {
|
||||
_print $"🛑 No (_ansi red_bold)target_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)"
|
||||
return false
|
||||
# Check if IP is valid IPv4
|
||||
export def is_valid_ipv4 [ip: string]: nothing -> bool {
|
||||
$ip =~ '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
|
||||
}
|
||||
let source_path = ($cluster.soruce_path | default "")
|
||||
if $source_path == "" {
|
||||
_print $"🛑 No (_ansi red_bold)source_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)"
|
||||
return false
|
||||
|
||||
# Check if IP is valid IPv6
|
||||
export def is_valid_ipv6 [ip: string]: nothing -> bool {
|
||||
$ip =~ ':[a-f0-9]{0,4}:' or $ip =~ '^[a-f0-9]{0,4}:[a-f0-9]{0,4}:'
|
||||
}
|
||||
if $local_mode {
|
||||
let res = (^cp $source_path $target_path | combine)
|
||||
if $res.exit_code != 0 {
|
||||
_print $"🛑 Error get_file [ local-mode ] (_ansi red_bold)($source_path) to ($target_path)(_ansi reset) in ($server.hostname) cluster ($cluster.name)"
|
||||
_print $res.stdout
|
||||
return false
|
||||
|
||||
# Format record as table for display
|
||||
export def format_server_table [servers: list]: nothing -> null {
|
||||
let columns = ["id", "name", "status", "public_net", "server_type"]
|
||||
|
||||
let formatted = $servers | map {|s|
|
||||
{
|
||||
ID: ($s.id | into string)
|
||||
Name: $s.name
|
||||
Status: ($s.status | str capitalize)
|
||||
IP: ($s.public_net.ipv4.ip | default "-")
|
||||
Type: ($s.server_type.name | default "-")
|
||||
Location: ($s.location.name | default "-")
|
||||
}
|
||||
return true
|
||||
}
|
||||
let ip = if $live_ip != "" {
|
||||
$live_ip
|
||||
|
||||
$formatted | table
|
||||
null
|
||||
}
|
||||
|
||||
# Get error message from API response
|
||||
export def extract_api_error [response: any]: nothing -> string {
|
||||
if ($response | has error) {
|
||||
if ($response.error | has message) {
|
||||
$response.error.message
|
||||
} else {
|
||||
#use ../../../providers/prov_lib/middleware.nu mw_get_ip
|
||||
(mw_get_ip $settings $server $server.liveness_ip false)
|
||||
($response.error | into string)
|
||||
}
|
||||
let ssh_key_path = ($server.ssh_key_path | default "")
|
||||
if $ssh_key_path == "" {
|
||||
_print $"🛑 No (_ansi red_bold)ssh_key_path(_ansi reset) found in ($server.hostname) cluster ($cluster.name)"
|
||||
return false
|
||||
}
|
||||
if not ($ssh_key_path | path exists) {
|
||||
_print $"🛑 Error (_ansi red_bold)($ssh_key_path)(_ansi reset) not found for ($server.hostname) cluster ($cluster.name)"
|
||||
return false
|
||||
}
|
||||
mut cmd = if $req_sudo { "sudo" } else { "" }
|
||||
let wk_path = $"/home/($env.SSH_USER)/($source_path| path basename)"
|
||||
$cmd = $"($cmd) cp ($source_path) ($wk_path); sudo chown ($env.SSH_USER) ($wk_path)"
|
||||
let wk_path = $"/home/($env.SSH_USER)/($source_path | path basename)"
|
||||
let res = (ssh_cmd $settings $server false $cmd $ip )
|
||||
if not $res { return false }
|
||||
if not (scp_from $settings $server $wk_path $target_path $ip ) {
|
||||
return false
|
||||
}
|
||||
let rm_cmd = if $req_sudo {
|
||||
$"sudo rm -f ($wk_path)"
|
||||
} else if ($response | has message) {
|
||||
$response.message
|
||||
} else {
|
||||
$"rm -f ($wk_path)"
|
||||
($response | into string)
|
||||
}
|
||||
}
|
||||
|
||||
# Validate server configuration
|
||||
export def validate_server_config [server: record]: nothing -> bool {
|
||||
let required = ["hostname", "server_type", "location"]
|
||||
let missing = $required | filter {|f| not ($server | has $f)}
|
||||
|
||||
if not ($missing | is-empty) {
|
||||
error make {msg: $"Missing required fields: ($missing | str join ", ")"}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
# Convert timestamp to human readable format
|
||||
export def format_timestamp [timestamp: int]: nothing -> string {
|
||||
let date = (date now | date to-record)
|
||||
$"($timestamp) (UTC)"
|
||||
}
|
||||
|
||||
# Retry function with exponential backoff
|
||||
export def retry_with_backoff [closure: closure, max_attempts: int = 3, initial_delay: int = 1]: nothing -> any {
|
||||
let mut attempts = 0
|
||||
let mut delay = $initial_delay
|
||||
|
||||
loop {
|
||||
try {
|
||||
return ($closure | call)
|
||||
} catch {|err|
|
||||
$attempts += 1
|
||||
|
||||
if $attempts >= $max_attempts {
|
||||
error make {msg: $"Operation failed after ($attempts) attempts: ($err.msg)"}
|
||||
}
|
||||
|
||||
print $"Attempt ($attempts) failed, retrying in ($delay) seconds..."
|
||||
sleep ($delay | into duration)
|
||||
$delay = $delay * 2
|
||||
}
|
||||
}
|
||||
return (ssh_cmd $settings $server false $rm_cmd $ip )
|
||||
}
|
||||
|
||||
@ -17,11 +17,10 @@ export def check_marimo_available []: nothing -> bool {
|
||||
export def install_marimo []: nothing -> bool {
|
||||
if not (check_marimo_available) {
|
||||
print "📦 Installing Marimo..."
|
||||
let result = do { ^pip install marimo } | complete
|
||||
|
||||
if $result.exit_code == 0 {
|
||||
try {
|
||||
^pip install marimo
|
||||
true
|
||||
} else {
|
||||
} catch {
|
||||
print "❌ Failed to install Marimo. Please install manually: pip install marimo"
|
||||
false
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ use polars_integration.nu *
|
||||
use ../lib_provisioning/utils/settings.nu *
|
||||
|
||||
# Log sources configuration
|
||||
export def get_log_sources []: nothing -> record {
|
||||
export def get_log_sources [] {
|
||||
{
|
||||
system: {
|
||||
paths: ["/var/log/syslog", "/var/log/messages"]
|
||||
@ -56,7 +56,7 @@ export def collect_logs [
|
||||
--output_format: string = "dataframe"
|
||||
--filter_level: string = "info"
|
||||
--include_metadata = true
|
||||
]: nothing -> any {
|
||||
] {
|
||||
|
||||
print $"📊 Collecting logs from the last ($since)..."
|
||||
|
||||
@ -100,7 +100,7 @@ def collect_from_source [
|
||||
source: string
|
||||
config: record
|
||||
--since: string = "1h"
|
||||
]: nothing -> list {
|
||||
] {
|
||||
|
||||
match $source {
|
||||
"system" => {
|
||||
@ -125,7 +125,7 @@ def collect_from_source [
|
||||
def collect_system_logs [
|
||||
config: record
|
||||
--since: string = "1h"
|
||||
]: record -> list {
|
||||
] {
|
||||
|
||||
$config.paths | each {|path|
|
||||
if ($path | path exists) {
|
||||
@ -142,7 +142,7 @@ def collect_system_logs [
|
||||
def collect_provisioning_logs [
|
||||
config: record
|
||||
--since: string = "1h"
|
||||
]: record -> list {
|
||||
] {
|
||||
|
||||
$config.paths | each {|log_dir|
|
||||
if ($log_dir | path exists) {
|
||||
@ -164,7 +164,7 @@ def collect_provisioning_logs [
|
||||
def collect_container_logs [
|
||||
config: record
|
||||
--since: string = "1h"
|
||||
]: record -> list {
|
||||
] {
|
||||
|
||||
if ((which docker | length) > 0) {
|
||||
collect_docker_logs --since $since
|
||||
@ -177,7 +177,7 @@ def collect_container_logs [
|
||||
def collect_kubernetes_logs [
|
||||
config: record
|
||||
--since: string = "1h"
|
||||
]: record -> list {
|
||||
] {
|
||||
|
||||
if ((which kubectl | length) > 0) {
|
||||
collect_k8s_logs --since $since
|
||||
@ -190,7 +190,7 @@ def collect_kubernetes_logs [
|
||||
def read_recent_logs [
|
||||
file_path: string
|
||||
--since: string = "1h"
|
||||
]: string -> list {
|
||||
] {
|
||||
|
||||
let since_timestamp = ((date now) - (parse_duration $since))
|
||||
|
||||
@ -213,7 +213,7 @@ def read_recent_logs [
|
||||
def parse_system_log_line [
|
||||
line: string
|
||||
source_file: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
# Parse standard syslog format
|
||||
let syslog_pattern = '(?P<timestamp>\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+(?P<hostname>\S+)\s+(?P<process>\S+?)(\[(?P<pid>\d+)\])?:\s*(?P<message>.*)'
|
||||
@ -246,7 +246,7 @@ def parse_system_log_line [
|
||||
def collect_json_logs [
|
||||
file_path: string
|
||||
--since: string = "1h"
|
||||
]: string -> list {
|
||||
] {
|
||||
|
||||
let lines = (read_recent_logs $file_path --since $since)
|
||||
$lines | each {|line|
|
||||
@ -278,7 +278,7 @@ def collect_json_logs [
|
||||
def collect_text_logs [
|
||||
file_path: string
|
||||
--since: string = "1h"
|
||||
]: string -> list {
|
||||
] {
|
||||
|
||||
let lines = (read_recent_logs $file_path --since $since)
|
||||
$lines | each {|line|
|
||||
@ -294,7 +294,7 @@ def collect_text_logs [
|
||||
|
||||
def collect_docker_logs [
|
||||
--since: string = "1h"
|
||||
]: nothing -> list {
|
||||
] {
|
||||
|
||||
do {
|
||||
let containers = (docker ps --format "{{.Names}}" | lines)
|
||||
@ -322,7 +322,7 @@ def collect_docker_logs [
|
||||
|
||||
def collect_k8s_logs [
|
||||
--since: string = "1h"
|
||||
]: nothing -> list {
|
||||
] {
|
||||
|
||||
do {
|
||||
let pods = (kubectl get pods -o jsonpath='{.items[*].metadata.name}' | split row " ")
|
||||
@ -348,7 +348,7 @@ def collect_k8s_logs [
|
||||
}
|
||||
}
|
||||
|
||||
def parse_syslog_timestamp [ts: string]: string -> datetime {
|
||||
def parse_syslog_timestamp [ts: string] {
|
||||
do {
|
||||
# Parse syslog timestamp format: "Jan 16 10:30:15"
|
||||
let current_year = (date now | date format "%Y")
|
||||
@ -360,7 +360,7 @@ def parse_syslog_timestamp [ts: string]: string -> datetime {
|
||||
}
|
||||
}
|
||||
|
||||
def extract_log_level [message: string]: string -> string {
|
||||
def extract_log_level [message: string] {
|
||||
let level_patterns = {
|
||||
"FATAL": "fatal"
|
||||
"ERROR": "error"
|
||||
@ -385,7 +385,7 @@ def extract_log_level [message: string]: string -> string {
|
||||
def filter_by_level [
|
||||
logs: list
|
||||
level: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
|
||||
let level_order = ["trace", "debug", "info", "warn", "warning", "error", "fatal"]
|
||||
let min_index = ($level_order | enumerate | where {|row| $row.item == $level} | get index.0)
|
||||
@ -396,7 +396,7 @@ def filter_by_level [
|
||||
}
|
||||
}
|
||||
|
||||
def parse_duration [duration: string]: string -> duration {
|
||||
def parse_duration [duration: string] {
|
||||
match $duration {
|
||||
$dur if ($dur | str ends-with "m") => {
|
||||
let minutes = ($dur | str replace "m" "" | into int)
|
||||
@ -422,7 +422,7 @@ export def analyze_logs [
|
||||
--analysis_type: string = "summary" # summary, errors, patterns, performance
|
||||
--time_window: string = "1h"
|
||||
--group_by: list<string> = ["service", "level"]
|
||||
]: any -> any {
|
||||
] {
|
||||
|
||||
match $analysis_type {
|
||||
"summary" => {
|
||||
@ -443,7 +443,7 @@ export def analyze_logs [
|
||||
}
|
||||
}
|
||||
|
||||
def analyze_log_summary [logs_df: any, group_cols: list<string>]: nothing -> any {
|
||||
def analyze_log_summary [logs_df: any, group_cols: list<string>] {
|
||||
aggregate_dataframe $logs_df --group_by $group_cols --operations {
|
||||
count: "count"
|
||||
first_seen: "min"
|
||||
@ -451,17 +451,17 @@ def analyze_log_summary [logs_df: any, group_cols: list<string>]: nothing -> any
|
||||
}
|
||||
}
|
||||
|
||||
def analyze_log_errors [logs_df: any]: any -> any {
|
||||
def analyze_log_errors [logs_df: any] {
|
||||
# Filter error logs and analyze patterns
|
||||
query_dataframe $logs_df "SELECT * FROM logs_df WHERE level IN ('error', 'fatal', 'warn')"
|
||||
}
|
||||
|
||||
def analyze_log_patterns [logs_df: any, time_window: string]: nothing -> any {
|
||||
def analyze_log_patterns [logs_df: any, time_window: string] {
|
||||
# Time series analysis of log patterns
|
||||
time_series_analysis $logs_df --time_column "timestamp" --value_column "level" --window $time_window
|
||||
}
|
||||
|
||||
def analyze_log_performance [logs_df: any, time_window: string]: nothing -> any {
|
||||
def analyze_log_performance [logs_df: any, time_window: string] {
|
||||
# Analyze performance-related logs
|
||||
query_dataframe $logs_df "SELECT * FROM logs_df WHERE message LIKE '%performance%' OR message LIKE '%slow%'"
|
||||
}
|
||||
@ -471,7 +471,7 @@ export def generate_log_report [
|
||||
logs_df: any
|
||||
--output_path: string = "log_report.md"
|
||||
--include_charts = false
|
||||
]: any -> nothing {
|
||||
] {
|
||||
|
||||
let summary = analyze_logs $logs_df --analysis_type "summary"
|
||||
let errors = analyze_logs $logs_df --analysis_type "errors"
|
||||
@ -516,7 +516,7 @@ export def monitor_logs [
|
||||
--follow = true
|
||||
--alert_level: string = "error"
|
||||
--callback: string = ""
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
|
||||
print $"👀 Starting real-time log monitoring (alert level: ($alert_level))..."
|
||||
|
||||
|
||||
@ -6,13 +6,13 @@
|
||||
use ../lib_provisioning/utils/settings.nu *
|
||||
|
||||
# Check if Polars plugin is available
|
||||
export def check_polars_available []: nothing -> bool {
|
||||
export def check_polars_available [] {
|
||||
let plugins = (plugin list)
|
||||
($plugins | any {|p| $p.name == "polars" or $p.name == "nu_plugin_polars"})
|
||||
}
|
||||
|
||||
# Initialize Polars plugin if available
|
||||
export def init_polars []: nothing -> bool {
|
||||
export def init_polars [] {
|
||||
if (check_polars_available) {
|
||||
# Polars plugin is available - return true
|
||||
# Note: Actual plugin loading happens during session initialization
|
||||
@ -28,7 +28,7 @@ export def create_infra_dataframe [
|
||||
data: list
|
||||
--source: string = "infrastructure"
|
||||
--timestamp = true
|
||||
]: list -> any {
|
||||
] {
|
||||
|
||||
let use_polars = init_polars
|
||||
|
||||
@ -56,7 +56,7 @@ export def process_logs_to_dataframe [
|
||||
--time_column: string = "timestamp"
|
||||
--level_column: string = "level"
|
||||
--message_column: string = "message"
|
||||
]: list<string> -> any {
|
||||
] {
|
||||
|
||||
let use_polars = init_polars
|
||||
|
||||
@ -100,7 +100,7 @@ export def process_logs_to_dataframe [
|
||||
def parse_log_file [
|
||||
file_path: string
|
||||
--format: string = "auto"
|
||||
]: string -> list {
|
||||
] {
|
||||
|
||||
if not ($file_path | path exists) {
|
||||
return []
|
||||
@ -167,7 +167,7 @@ def parse_log_file [
|
||||
}
|
||||
|
||||
# Parse syslog format line
|
||||
def parse_syslog_line [line: string]: string -> record {
|
||||
def parse_syslog_line [line: string] {
|
||||
# Basic syslog parsing - can be enhanced
|
||||
let parts = ($line | parse --regex '(?P<timestamp>\w+\s+\d+\s+\d+:\d+:\d+)\s+(?P<host>\S+)\s+(?P<service>\S+):\s*(?P<message>.*)')
|
||||
|
||||
@ -190,7 +190,7 @@ def parse_syslog_line [line: string]: string -> record {
|
||||
}
|
||||
|
||||
# Standardize timestamp formats
|
||||
def standardize_timestamp [ts: any]: any -> datetime {
|
||||
def standardize_timestamp [ts: any] {
|
||||
match ($ts | describe) {
|
||||
"string" => {
|
||||
do {
|
||||
@ -207,14 +207,14 @@ def standardize_timestamp [ts: any]: any -> datetime {
|
||||
}
|
||||
|
||||
# Enhance Nushell table with DataFrame-like operations
|
||||
def enhance_nushell_table []: list -> list {
|
||||
def enhance_nushell_table [] {
|
||||
let data = $in
|
||||
|
||||
# Add DataFrame-like methods through custom commands
|
||||
$data | add_dataframe_methods
|
||||
}
|
||||
|
||||
def add_dataframe_methods []: list -> list {
|
||||
def add_dataframe_methods [] {
|
||||
# This function adds metadata to enable DataFrame-like operations
|
||||
# In a real implementation, we'd add custom commands to the scope
|
||||
$in
|
||||
@ -225,7 +225,7 @@ export def query_dataframe [
|
||||
df: any
|
||||
query: string
|
||||
--use_polars = false
|
||||
]: any -> any {
|
||||
] {
|
||||
|
||||
if $use_polars and (check_polars_available) {
|
||||
# Use Polars query capabilities
|
||||
@ -236,7 +236,7 @@ export def query_dataframe [
|
||||
}
|
||||
}
|
||||
|
||||
def query_with_nushell [df: any, query: string]: nothing -> any {
|
||||
def query_with_nushell [df: any, query: string] {
|
||||
# Simple SQL-like query parser for Nushell
|
||||
# This is a basic implementation - can be significantly enhanced
|
||||
|
||||
@ -266,7 +266,7 @@ def query_with_nushell [df: any, query: string]: nothing -> any {
|
||||
}
|
||||
}
|
||||
|
||||
def process_where_clause [data: any, conditions: string]: nothing -> any {
|
||||
def process_where_clause [data: any, conditions: string] {
|
||||
# Basic WHERE clause implementation
|
||||
# This would need significant enhancement for production use
|
||||
$data
|
||||
@ -278,7 +278,7 @@ export def aggregate_dataframe [
|
||||
--group_by: list<string> = []
|
||||
--operations: record = {} # {column: operation}
|
||||
--time_bucket: string = "1h" # For time-based aggregations
|
||||
]: any -> any {
|
||||
] {
|
||||
|
||||
let use_polars = init_polars
|
||||
|
||||
@ -296,7 +296,7 @@ def aggregate_with_polars [
|
||||
group_cols: list<string>
|
||||
operations: record
|
||||
time_bucket: string
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# Polars aggregation implementation
|
||||
if ($group_cols | length) > 0 {
|
||||
$df | polars group-by $group_cols | polars agg [
|
||||
@ -314,7 +314,7 @@ def aggregate_with_nushell [
|
||||
group_cols: list<string>
|
||||
operations: record
|
||||
time_bucket: string
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# Nushell aggregation implementation
|
||||
if ($group_cols | length) > 0 {
|
||||
$df | group-by ($group_cols | str join " ")
|
||||
@ -330,7 +330,7 @@ export def time_series_analysis [
|
||||
--value_column: string = "value"
|
||||
--window: string = "1h"
|
||||
--operations: list<string> = ["mean", "sum", "count"]
|
||||
]: any -> any {
|
||||
] {
|
||||
|
||||
let use_polars = init_polars
|
||||
|
||||
@ -347,7 +347,7 @@ def time_series_with_polars [
|
||||
value_col: string
|
||||
window: string
|
||||
ops: list<string>
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# Polars time series operations
|
||||
$df | polars group-by $time_col | polars agg [
|
||||
(polars col $value_col | polars mean)
|
||||
@ -362,7 +362,7 @@ def time_series_with_nushell [
|
||||
value_col: string
|
||||
window: string
|
||||
ops: list<string>
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# Nushell time series - basic implementation
|
||||
$df | group-by {|row|
|
||||
# Group by time windows - simplified
|
||||
@ -383,7 +383,7 @@ export def export_dataframe [
|
||||
df: any
|
||||
output_path: string
|
||||
--format: string = "csv" # csv, parquet, json, excel
|
||||
]: any -> nothing {
|
||||
] {
|
||||
|
||||
let use_polars = init_polars
|
||||
|
||||
@ -417,7 +417,7 @@ export def export_dataframe [
|
||||
export def benchmark_operations [
|
||||
data_size: int = 10000
|
||||
operations: list<string> = ["filter", "group", "aggregate"]
|
||||
]: int -> record {
|
||||
] {
|
||||
|
||||
print $"🔬 Benchmarking operations with ($data_size) records..."
|
||||
|
||||
@ -462,7 +462,7 @@ export def benchmark_operations [
|
||||
$results
|
||||
}
|
||||
|
||||
def benchmark_nushell_operations [data: list, ops: list<string>]: nothing -> any {
|
||||
def benchmark_nushell_operations [data: list, ops: list<string>] {
|
||||
mut result = $data
|
||||
|
||||
if "filter" in $ops {
|
||||
@ -484,7 +484,7 @@ def benchmark_nushell_operations [data: list, ops: list<string>]: nothing -> any
|
||||
$result
|
||||
}
|
||||
|
||||
def benchmark_polars_operations [data: list, ops: list<string>]: nothing -> any {
|
||||
def benchmark_polars_operations [data: list, ops: list<string>] {
|
||||
mut df = ($data | polars into-df)
|
||||
|
||||
if "filter" in $ops {
|
||||
|
||||
@ -256,7 +256,7 @@ export-env {
|
||||
}
|
||||
|
||||
export def "show_env" [
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let env_vars = {
|
||||
PROVISIONING: $env.PROVISIONING,
|
||||
PROVISIONING_CORE: $env.PROVISIONING_CORE,
|
||||
|
||||
@ -1,16 +1,147 @@
|
||||
#!/usr/bin/env nu
|
||||
# Minimal Help System - Fast Path without Config Loading
|
||||
# Minimal Help System - Fast Path with Fluent i18n Support
|
||||
# This bypasses the full config system for instant help display
|
||||
# Uses Nushell's built-in ansi function for ANSI color codes
|
||||
# Uses Mozilla Fluent (.ftl) format for multilingual support
|
||||
|
||||
# Main help dispatcher - no config needed
|
||||
def provisioning-help [category?: string = ""]: nothing -> string {
|
||||
# If no category provided, show main help
|
||||
|
||||
|
||||
# Format alias: brackets in gray, inner text in category color
|
||||
def format-alias [alias: string, color: string] {
|
||||
if ($alias | is-empty) {
|
||||
""
|
||||
} else if ($alias | str starts-with "[") and ($alias | str ends-with "]") {
|
||||
# Extract content between brackets (exclusive end range)
|
||||
let inner = ($alias | str substring 1..<(-1))
|
||||
(ansi d) + "[" + (ansi rst) + $color + $inner + (ansi rst) + (ansi d) + "]" + (ansi rst)
|
||||
} else {
|
||||
(ansi d) + $alias + (ansi rst)
|
||||
}
|
||||
}
|
||||
|
||||
# Format categories with tab-separated columns and colors
|
||||
def format-categories [rows: list<list<string>>] {
|
||||
let header = " Category\t\tAlias\t Description"
|
||||
let separator = " ════════════════════════════════════════════════════════════════════"
|
||||
|
||||
let formatted_rows = (
|
||||
$rows | each { |row|
|
||||
let emoji = $row.0
|
||||
let name = $row.1
|
||||
let alias = $row.2
|
||||
let desc = $row.3
|
||||
|
||||
# Assign color based on category name
|
||||
let color = (match $name {
|
||||
"infrastructure" => (ansi cyan)
|
||||
"orchestration" => (ansi magenta)
|
||||
"development" => (ansi green)
|
||||
"workspace" => (ansi green)
|
||||
"setup" => (ansi magenta)
|
||||
"platform" => (ansi red)
|
||||
"authentication" => (ansi yellow)
|
||||
"plugins" => (ansi cyan)
|
||||
"utilities" => (ansi green)
|
||||
"tools" => (ansi yellow)
|
||||
"vm" => (ansi white)
|
||||
"diagnostics" => (ansi magenta)
|
||||
"concepts" => (ansi yellow)
|
||||
"guides" => (ansi blue)
|
||||
"integrations" => (ansi cyan)
|
||||
_ => ""
|
||||
})
|
||||
|
||||
# Calculate tabs based on name length: 3 tabs for 6-10 char names, 2 tabs otherwise
|
||||
let name_len = ($name | str length)
|
||||
let name_tabs = match true {
|
||||
_ if $name_len <= 11 => "\t\t"
|
||||
_ => "\t"
|
||||
}
|
||||
|
||||
# Format alias with brackets in gray and inner text in category color
|
||||
let alias_formatted = (format-alias $alias $color)
|
||||
let alias_len = ($alias | str length)
|
||||
let alias_tabs = match true {
|
||||
_ if ($alias_len == 8) => ""
|
||||
_ if ($name_len <= 3) => "\t\t"
|
||||
_ => "\t"
|
||||
}
|
||||
# Format: emoji + colored_name + tabs + colored_alias + tabs + description
|
||||
$" ($emoji)($color)($name)((ansi rst))($name_tabs)($alias_formatted)($alias_tabs) ($desc)"
|
||||
}
|
||||
)
|
||||
|
||||
([$header, $separator] | append $formatted_rows | str join "\n")
|
||||
}
|
||||
|
||||
# Get active locale from LANG environment variable
|
||||
def get-active-locale [] {
|
||||
let lang_env = ($env.LANG? | default "en_US")
|
||||
let dot_idx = ($lang_env | str index-of ".")
|
||||
let lang_part = (
|
||||
if $dot_idx >= 0 {
|
||||
$lang_env | str substring 0..<$dot_idx
|
||||
} else {
|
||||
$lang_env
|
||||
}
|
||||
)
|
||||
let locale = ($lang_part | str replace "_" "-")
|
||||
$locale
|
||||
}
|
||||
|
||||
# Parse simple Fluent format and return record of strings
|
||||
def parse-fluent [content: string] {
|
||||
let lines = (
|
||||
$content
|
||||
| str replace (char newline) "\n"
|
||||
| split row "\n"
|
||||
)
|
||||
|
||||
$lines | reduce -f {} { |line, strings|
|
||||
if ($line | str starts-with "#") or ($line | str trim | is-empty) {
|
||||
$strings
|
||||
} else if ($line | str contains " = ") {
|
||||
let idx = ($line | str index-of " = ")
|
||||
if $idx != null {
|
||||
let key = ($line | str substring 0..$idx | str trim)
|
||||
let value = ($line | str substring ($idx + 3).. | str trim | str trim -c "\"")
|
||||
$strings | insert $key $value
|
||||
} else {
|
||||
$strings
|
||||
}
|
||||
} else {
|
||||
$strings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get a help string with fallback to English
|
||||
def get-help-string [key: string] {
|
||||
let locale = (get-active-locale)
|
||||
# Use environment variable PROVISIONING as base path
|
||||
let prov_path = ($env.PROVISIONING? | default "/usr/local/provisioning/provisioning")
|
||||
let base_path = $"($prov_path)/locales"
|
||||
|
||||
let locale_file = $"($base_path)/($locale)/help.ftl"
|
||||
let fallback_file = $"($base_path)/en-US/help.ftl"
|
||||
|
||||
let content = (
|
||||
if ($locale_file | path exists) {
|
||||
open $locale_file
|
||||
} else {
|
||||
open $fallback_file
|
||||
}
|
||||
)
|
||||
|
||||
let strings = (parse-fluent $content)
|
||||
$strings | get $key | default "[$key]"
|
||||
}
|
||||
|
||||
# Main help dispatcher
|
||||
def provisioning-help [category?: string = ""] {
|
||||
if ($category == "") {
|
||||
return (help-main)
|
||||
}
|
||||
|
||||
# Try to match the category
|
||||
let cat_lower = ($category | str downcase)
|
||||
let result = (match $cat_lower {
|
||||
"infrastructure" | "infra" => "infrastructure"
|
||||
@ -32,7 +163,6 @@ def provisioning-help [category?: string = ""]: nothing -> string {
|
||||
_ => "unknown"
|
||||
})
|
||||
|
||||
# If unknown category, show error
|
||||
if $result == "unknown" {
|
||||
print $"❌ Unknown help category: \"($category)\"\n"
|
||||
print "Available help categories: infrastructure, orchestration, development, workspace, setup, platform,"
|
||||
@ -40,7 +170,6 @@ def provisioning-help [category?: string = ""]: nothing -> string {
|
||||
return ""
|
||||
}
|
||||
|
||||
# Match valid category
|
||||
match $result {
|
||||
"infrastructure" => (help-infrastructure)
|
||||
"orchestration" => (help-orchestration)
|
||||
@ -63,374 +192,384 @@ def provisioning-help [category?: string = ""]: nothing -> string {
|
||||
}
|
||||
|
||||
# Main help overview
|
||||
def help-main []: nothing -> string {
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "╔════════════════════════════════════════════════════════════════╗" + (ansi rst) + "\n" +
|
||||
(ansi yellow) + (ansi bo) + "║" + (ansi rst) + " " + (ansi cyan) + (ansi bo) + "PROVISIONING SYSTEM" + (ansi rst) + " - Layered Infrastructure Automation " + (ansi yellow) + (ansi bo) + " ║" + (ansi rst) + "\n" +
|
||||
(ansi yellow) + (ansi bo) + "╚════════════════════════════════════════════════════════════════╝" + (ansi rst) + "\n\n" +
|
||||
def help-main [] {
|
||||
let title = (get-help-string "help-main-title")
|
||||
let subtitle = (get-help-string "help-main-subtitle")
|
||||
let categories = (get-help-string "help-main-categories")
|
||||
let hint = (get-help-string "help-main-categories-hint")
|
||||
|
||||
(ansi green) + (ansi bo) + "📚 COMMAND CATEGORIES" + (ansi rst) + " " + (ansi d) + "- Use 'provisioning help <category>' for details" + (ansi rst) + "\n\n" +
|
||||
let infra_desc = (get-help-string "help-main-infrastructure-desc")
|
||||
let orch_desc = (get-help-string "help-main-orchestration-desc")
|
||||
let dev_desc = (get-help-string "help-main-development-desc")
|
||||
let ws_desc = (get-help-string "help-main-workspace-desc")
|
||||
let plat_desc = (get-help-string "help-main-platform-desc")
|
||||
let setup_desc = (get-help-string "help-main-setup-desc")
|
||||
let auth_desc = (get-help-string "help-main-authentication-desc")
|
||||
let plugins_desc = (get-help-string "help-main-plugins-desc")
|
||||
let utils_desc = (get-help-string "help-main-utilities-desc")
|
||||
let tools_desc = (get-help-string "help-main-tools-desc")
|
||||
let vm_desc = (get-help-string "help-main-vm-desc")
|
||||
let diag_desc = (get-help-string "help-main-diagnostics-desc")
|
||||
let concepts_desc = (get-help-string "help-main-concepts-desc")
|
||||
let guides_desc = (get-help-string "help-main-guides-desc")
|
||||
let int_desc = (get-help-string "help-main-integrations-desc")
|
||||
|
||||
" " + (ansi cyan) + "🏗️ infrastructure" + (ansi rst) + " " + (ansi d) + "[infra]" + (ansi rst) + "\t\t Server, taskserv, cluster, VM, and infra management\n" +
|
||||
" " + (ansi magenta) + "⚡ orchestration" + (ansi rst) + " " + (ansi d) + "[orch]" + (ansi rst) + "\t\t Workflow, batch operations, and orchestrator control\n" +
|
||||
" " + (ansi blue) + "🧩 development" + (ansi rst) + " " + (ansi d) + "[dev]" + (ansi rst) + "\t\t\t Module discovery, layers, versions, and packaging\n" +
|
||||
" " + (ansi green) + "📁 workspace" + (ansi rst) + " " + (ansi d) + "[ws]" + (ansi rst) + "\t\t\t Workspace and template management\n" +
|
||||
" " + (ansi magenta) + "⚙️ setup" + (ansi rst) + " " + (ansi d) + "[st]" + (ansi rst) + "\t\t\t\t System setup, configuration, and initialization\n" +
|
||||
" " + (ansi red) + "🖥️ platform" + (ansi rst) + " " + (ansi d) + "[plat]" + (ansi rst) + "\t\t\t Orchestrator, Control Center UI, MCP Server\n" +
|
||||
" " + (ansi yellow) + "🔐 authentication" + (ansi rst) + " " + (ansi d) + "[auth]" + (ansi rst) + "\t\t JWT authentication, MFA, and sessions\n" +
|
||||
" " + (ansi cyan) + "🔌 plugins" + (ansi rst) + " " + (ansi d) + "[plugin]" + (ansi rst) + "\t\t\t Plugin management and integration\n" +
|
||||
" " + (ansi green) + "🛠️ utilities" + (ansi rst) + " " + (ansi d) + "[utils]" + (ansi rst) + "\t\t\t Cache, SOPS editing, providers, plugins, SSH\n" +
|
||||
" " + (ansi yellow) + "🌉 integrations" + (ansi rst) + " " + (ansi d) + "[int]" + (ansi rst) + "\t\t\t Prov-ecosystem and provctl bridge\n" +
|
||||
" " + (ansi green) + "🔍 diagnostics" + (ansi rst) + " " + (ansi d) + "[diag]" + (ansi rst) + "\t\t\t System status, health checks, and next steps\n" +
|
||||
" " + (ansi magenta) + "📚 guides" + (ansi rst) + " " + (ansi d) + "[guide]" + (ansi rst) + "\t\t\t Quick guides and cheatsheets\n" +
|
||||
" " + (ansi yellow) + "💡 concepts" + (ansi rst) + " " + (ansi d) + "[concept]" + (ansi rst) + "\t\t\t Understanding layers, modules, and architecture\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "🚀 QUICK START" + (ansi rst) + "\n\n" +
|
||||
" 1. " + (ansi cyan) + "Understand the system" + (ansi rst) + ": provisioning help concepts\n" +
|
||||
" 2. " + (ansi cyan) + "Create workspace" + (ansi rst) + ": provisioning workspace init my-infra --activate\n" +
|
||||
" " + (ansi cyan) + "Or use interactive:" + (ansi rst) + " provisioning workspace init --interactive\n" +
|
||||
" 3. " + (ansi cyan) + "Discover modules" + (ansi rst) + ": provisioning module discover taskservs\n" +
|
||||
" 4. " + (ansi cyan) + "Create servers" + (ansi rst) + ": provisioning server create --infra my-infra\n" +
|
||||
" 5. " + (ansi cyan) + "Deploy services" + (ansi rst) + ": provisioning taskserv create kubernetes\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "🔧 COMMON COMMANDS" + (ansi rst) + "\n\n" +
|
||||
" provisioning server list - List all servers\n" +
|
||||
" provisioning workflow list - List workflows\n" +
|
||||
" provisioning module discover taskservs - Discover available taskservs\n" +
|
||||
" provisioning layer show <workspace> - Show layer resolution\n" +
|
||||
" provisioning config validate - Validate configuration\n" +
|
||||
" provisioning help <category> - Get help on a topic\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "ℹ️ HELP TOPICS" + (ansi rst) + "\n\n" +
|
||||
" provisioning help infrastructure " + (ansi d) + "[or: infra]" + (ansi rst) + " - Server/cluster lifecycle\n" +
|
||||
" provisioning help orchestration " + (ansi d) + "[or: orch]" + (ansi rst) + " - Workflows and batch operations\n" +
|
||||
" provisioning help development " + (ansi d) + "[or: dev]" + (ansi rst) + " - Module system and tools\n" +
|
||||
" provisioning help workspace " + (ansi d) + "[or: ws]" + (ansi rst) + " - Workspace management\n" +
|
||||
" provisioning help setup " + (ansi d) + "[or: st]" + (ansi rst) + " - System setup and configuration\n" +
|
||||
" provisioning help platform " + (ansi d) + "[or: plat]" + (ansi rst) + " - Platform services\n" +
|
||||
" provisioning help authentication " + (ansi d) + "[or: auth]" + (ansi rst) + " - Authentication system\n" +
|
||||
" provisioning help utilities " + (ansi d) + "[or: utils]" + (ansi rst) + " - Cache, SOPS, providers, utilities\n" +
|
||||
" provisioning help guides " + (ansi d) + "[or: guide]" + (ansi rst) + " - Step-by-step guides\n"
|
||||
# Build output string
|
||||
let header = (
|
||||
(ansi yellow) + "════════════════════════════════════════════════════════════════════════════" + (ansi rst) + "\n" +
|
||||
" " + (ansi cyan) + (ansi bo) + ($title) + (ansi rst) + " - " + ($subtitle) + "\n" +
|
||||
(ansi yellow) + "════════════════════════════════════════════════════════════════════════════" + (ansi rst) + "\n\n"
|
||||
)
|
||||
|
||||
let categories_header = (
|
||||
(ansi green) + (ansi bo) + "📚 " + ($categories) + (ansi rst) + " " + (ansi d) + "- " + ($hint) + (ansi rst) + "\n\n"
|
||||
)
|
||||
|
||||
# Build category rows: [emoji, name, alias, description]
|
||||
let rows = [
|
||||
["🏗️", "infrastructure", "[infra]", $infra_desc],
|
||||
["⚡", "orchestration", "[orch]", $orch_desc],
|
||||
["🧩", "development", "[dev]", $dev_desc],
|
||||
["📁", "workspace", "[ws]", $ws_desc],
|
||||
["⚙️", "setup", "[st]", $setup_desc],
|
||||
["🖥️", "platform", "[plat]", $plat_desc],
|
||||
["🔐", "authentication", "[auth]", $auth_desc],
|
||||
["🔌", "plugins", "[plugin]", $plugins_desc],
|
||||
["🛠️", "utilities", "[utils]", $utils_desc],
|
||||
["🌉", "tools", "", $tools_desc],
|
||||
["🔍", "vm", "", $vm_desc],
|
||||
["📚", "diagnostics", "[diag]", $diag_desc],
|
||||
["💡", "concepts", "", $concepts_desc],
|
||||
["📖", "guides", "[guide]", $guides_desc],
|
||||
["🌐", "integrations", "[int]", $int_desc],
|
||||
]
|
||||
|
||||
let categories_table = (format-categories $rows)
|
||||
|
||||
print ($header + $categories_header + $categories_table)
|
||||
}
|
||||
|
||||
# Infrastructure help
|
||||
def help-infrastructure []: nothing -> string {
|
||||
def help-infrastructure [] {
|
||||
let title = (get-help-string "help-infrastructure-title")
|
||||
let intro = (get-help-string "help-infra-intro")
|
||||
let server_header = (get-help-string "help-infra-server-header")
|
||||
let server_create = (get-help-string "help-infra-server-create")
|
||||
let server_list = (get-help-string "help-infra-server-list")
|
||||
let server_delete = (get-help-string "help-infra-server-delete")
|
||||
let server_ssh = (get-help-string "help-infra-server-ssh")
|
||||
let server_price = (get-help-string "help-infra-server-price")
|
||||
let taskserv_header = (get-help-string "help-infra-taskserv-header")
|
||||
let taskserv_create = (get-help-string "help-infra-taskserv-create")
|
||||
let taskserv_delete = (get-help-string "help-infra-taskserv-delete")
|
||||
let taskserv_list = (get-help-string "help-infra-taskserv-list")
|
||||
let taskserv_generate = (get-help-string "help-infra-taskserv-generate")
|
||||
let taskserv_updates = (get-help-string "help-infra-taskserv-updates")
|
||||
let cluster_header = (get-help-string "help-infra-cluster-header")
|
||||
let cluster_create = (get-help-string "help-infra-cluster-create")
|
||||
let cluster_delete = (get-help-string "help-infra-cluster-delete")
|
||||
let cluster_list = (get-help-string "help-infra-cluster-list")
|
||||
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "INFRASTRUCTURE MANAGEMENT" + (ansi rst) + "\n\n" +
|
||||
"Manage servers, taskservs, clusters, and VMs across your infrastructure.\n\n" +
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "SERVER COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning server create --infra <name> - Create new server\n" +
|
||||
" provisioning server list - List all servers\n" +
|
||||
" provisioning server delete <server> - Delete a server\n" +
|
||||
" provisioning server ssh <server> - SSH into server\n" +
|
||||
" provisioning server price - Show server pricing\n\n" +
|
||||
(ansi green) + (ansi bo) + ($server_header) + (ansi rst) + "\n" +
|
||||
$" provisioning server create --infra <name> - ($server_create)\n" +
|
||||
$" provisioning server list - ($server_list)\n" +
|
||||
$" provisioning server delete <server> - ($server_delete)\n" +
|
||||
$" provisioning server ssh <server> - ($server_ssh)\n" +
|
||||
$" provisioning server price - ($server_price)\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "TASKSERV COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning taskserv create <type> - Create taskserv\n" +
|
||||
" provisioning taskserv delete <type> - Delete taskserv\n" +
|
||||
" provisioning taskserv list - List taskservs\n" +
|
||||
" provisioning taskserv generate <type> - Generate taskserv config\n" +
|
||||
" provisioning taskserv check-updates - Check for updates\n\n" +
|
||||
(ansi green) + (ansi bo) + ($taskserv_header) + (ansi rst) + "\n" +
|
||||
$" provisioning taskserv create <type> - ($taskserv_create)\n" +
|
||||
$" provisioning taskserv delete <type> - ($taskserv_delete)\n" +
|
||||
$" provisioning taskserv list - ($taskserv_list)\n" +
|
||||
$" provisioning taskserv generate <type> - ($taskserv_generate)\n" +
|
||||
$" provisioning taskserv check-updates - ($taskserv_updates)\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "CLUSTER COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning cluster create <name> - Create cluster\n" +
|
||||
" provisioning cluster delete <name> - Delete cluster\n" +
|
||||
" provisioning cluster list - List clusters\n"
|
||||
(ansi green) + (ansi bo) + ($cluster_header) + (ansi rst) + "\n" +
|
||||
$" provisioning cluster create <name> - ($cluster_create)\n" +
|
||||
$" provisioning cluster delete <name> - ($cluster_delete)\n" +
|
||||
$" provisioning cluster list - ($cluster_list)\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Orchestration help
|
||||
def help-orchestration []: nothing -> string {
|
||||
def help-orchestration [] {
|
||||
let title = (get-help-string "help-orchestration-title")
|
||||
let intro = (get-help-string "help-orch-intro")
|
||||
let workflows_header = (get-help-string "help-orch-workflows-header")
|
||||
let workflow_list = (get-help-string "help-orch-workflow-list")
|
||||
let workflow_status = (get-help-string "help-orch-workflow-status")
|
||||
let workflow_monitor = (get-help-string "help-orch-workflow-monitor")
|
||||
let workflow_stats = (get-help-string "help-orch-workflow-stats")
|
||||
let batch_header = (get-help-string "help-orch-batch-header")
|
||||
let batch_submit = (get-help-string "help-orch-batch-submit")
|
||||
let batch_list = (get-help-string "help-orch-batch-list")
|
||||
let batch_status = (get-help-string "help-orch-batch-status")
|
||||
let control_header = (get-help-string "help-orch-control-header")
|
||||
let orch_start = (get-help-string "help-orch-start")
|
||||
let orch_stop = (get-help-string "help-orch-stop")
|
||||
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "ORCHESTRATION AND WORKFLOWS" + (ansi rst) + "\n\n" +
|
||||
"Manage workflows, batch operations, and orchestrator services.\n\n" +
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "WORKFLOW COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning workflow list - List workflows\n" +
|
||||
" provisioning workflow status <id> - Get workflow status\n" +
|
||||
" provisioning workflow monitor <id> - Monitor workflow progress\n" +
|
||||
" provisioning workflow stats - Show workflow statistics\n\n" +
|
||||
(ansi green) + (ansi bo) + ($workflows_header) + (ansi rst) + "\n" +
|
||||
$" provisioning workflow list - ($workflow_list)\n" +
|
||||
$" provisioning workflow status <id> - ($workflow_status)\n" +
|
||||
$" provisioning workflow monitor <id> - ($workflow_monitor)\n" +
|
||||
$" provisioning workflow stats - ($workflow_stats)\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "BATCH COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning batch submit <file> - Submit batch workflow\n" +
|
||||
" provisioning batch list - List batches\n" +
|
||||
" provisioning batch status <id> - Get batch status\n\n" +
|
||||
(ansi green) + (ansi bo) + ($batch_header) + (ansi rst) + "\n" +
|
||||
$" provisioning batch submit <file> - ($batch_submit)\n" +
|
||||
$" provisioning batch list - ($batch_list)\n" +
|
||||
$" provisioning batch status <id> - ($batch_status)\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "ORCHESTRATOR COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning orchestrator start - Start orchestrator\n" +
|
||||
" provisioning orchestrator stop - Stop orchestrator\n"
|
||||
(ansi green) + (ansi bo) + ($control_header) + (ansi rst) + "\n" +
|
||||
$" provisioning orchestrator start - ($orch_start)\n" +
|
||||
$" provisioning orchestrator stop - ($orch_stop)\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Setup help with full Fluent support
|
||||
def help-setup [] {
|
||||
let title = (get-help-string "help-setup-title")
|
||||
let intro = (get-help-string "help-setup-intro")
|
||||
let initial = (get-help-string "help-setup-initial")
|
||||
let system = (get-help-string "help-setup-system")
|
||||
let system_desc = (get-help-string "help-setup-system-desc")
|
||||
let workspace_header = (get-help-string "help-setup-workspace-header")
|
||||
let workspace_cmd = (get-help-string "help-setup-workspace-cmd")
|
||||
let workspace_desc = (get-help-string "help-setup-workspace-desc")
|
||||
let workspace_init = (get-help-string "help-setup-workspace-init")
|
||||
let provider_header = (get-help-string "help-setup-provider-header")
|
||||
let provider_cmd = (get-help-string "help-setup-provider-cmd")
|
||||
let provider_desc = (get-help-string "help-setup-provider-desc")
|
||||
let provider_support = (get-help-string "help-setup-provider-support")
|
||||
let platform_header = (get-help-string "help-setup-platform-header")
|
||||
let platform_cmd = (get-help-string "help-setup-platform-cmd")
|
||||
let platform_desc = (get-help-string "help-setup-platform-desc")
|
||||
let platform_services = (get-help-string "help-setup-platform-services")
|
||||
let modes = (get-help-string "help-setup-modes")
|
||||
let interactive = (get-help-string "help-setup-interactive")
|
||||
let config = (get-help-string "help-setup-config")
|
||||
let defaults = (get-help-string "help-setup-defaults")
|
||||
let phases = (get-help-string "help-setup-phases")
|
||||
let phase_1 = (get-help-string "help-setup-phase-1")
|
||||
let phase_2 = (get-help-string "help-setup-phase-2")
|
||||
let phase_3 = (get-help-string "help-setup-phase-3")
|
||||
let phase_4 = (get-help-string "help-setup-phase-4")
|
||||
let phase_5 = (get-help-string "help-setup-phase-5")
|
||||
let security = (get-help-string "help-setup-security")
|
||||
let security_vault = (get-help-string "help-setup-security-vault")
|
||||
let security_sops = (get-help-string "help-setup-security-sops")
|
||||
let security_cedar = (get-help-string "help-setup-security-cedar")
|
||||
let examples = (get-help-string "help-setup-examples")
|
||||
let example_system = (get-help-string "help-setup-example-system")
|
||||
let example_workspace = (get-help-string "help-setup-example-workspace")
|
||||
let example_provider = (get-help-string "help-setup-example-provider")
|
||||
let example_platform = (get-help-string "help-setup-example-platform")
|
||||
|
||||
(
|
||||
(ansi magenta) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($initial) + (ansi rst) + "\n" +
|
||||
" provisioning setup system - " + ($system) + "\n" +
|
||||
" " + ($system_desc) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($workspace_header) + (ansi rst) + "\n" +
|
||||
" " + ($workspace_cmd) + " - " + ($workspace_desc) + "\n" +
|
||||
" " + ($workspace_init) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($provider_header) + (ansi rst) + "\n" +
|
||||
" " + ($provider_cmd) + " - " + ($provider_desc) + "\n" +
|
||||
" " + ($provider_support) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($platform_header) + (ansi rst) + "\n" +
|
||||
" " + ($platform_cmd) + " - " + ($platform_desc) + "\n" +
|
||||
" " + ($platform_services) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($modes) + (ansi rst) + "\n" +
|
||||
" " + ($interactive) + "\n" +
|
||||
" " + ($config) + "\n" +
|
||||
" " + ($defaults) + "\n\n" +
|
||||
|
||||
(ansi cyan) + ($phases) + (ansi rst) + "\n" +
|
||||
" " + ($phase_1) + "\n" +
|
||||
" " + ($phase_2) + "\n" +
|
||||
" " + ($phase_3) + "\n" +
|
||||
" " + ($phase_4) + "\n" +
|
||||
" " + ($phase_5) + "\n\n" +
|
||||
|
||||
(ansi cyan) + ($security) + (ansi rst) + "\n" +
|
||||
" " + ($security_vault) + "\n" +
|
||||
" " + ($security_sops) + "\n" +
|
||||
" " + ($security_cedar) + "\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + ($examples) + (ansi rst) + "\n" +
|
||||
" " + ($example_system) + "\n" +
|
||||
" " + ($example_workspace) + "\n" +
|
||||
" " + ($example_provider) + "\n" +
|
||||
" " + ($example_platform) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Development help
|
||||
def help-development []: nothing -> string {
|
||||
def help-development [] {
|
||||
let title = (get-help-string "help-development-title")
|
||||
let intro = (get-help-string "help-development-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "DEVELOPMENT AND MODULES" + (ansi rst) + "\n\n" +
|
||||
"Manage modules, layers, versions, and packaging.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "MODULE COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning module discover <type> - Discover available modules\n" +
|
||||
" provisioning module load <name> - Load a module\n" +
|
||||
" provisioning module list - List loaded modules\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "LAYER COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning layer show <workspace> - Show layer resolution\n" +
|
||||
" provisioning layer test <layer> - Test a layer\n"
|
||||
(ansi blue) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Workspace help
|
||||
def help-workspace []: nothing -> string {
|
||||
def help-workspace [] {
|
||||
let title = (get-help-string "help-workspace-title")
|
||||
let intro = (get-help-string "help-workspace-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "WORKSPACE MANAGEMENT" + (ansi rst) + "\n\n" +
|
||||
"Initialize, switch, and manage workspaces.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "WORKSPACE COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning workspace init [name] - Initialize new workspace\n" +
|
||||
" provisioning workspace list - List all workspaces\n" +
|
||||
" provisioning workspace active - Show active workspace\n" +
|
||||
" provisioning workspace activate <name> - Activate workspace\n"
|
||||
(ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Platform help
|
||||
def help-platform []: nothing -> string {
|
||||
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 yellow) + (ansi bo) + "PLATFORM SERVICES" + (ansi rst) + "\n\n" +
|
||||
"Manage orchestrator, control center, and MCP services.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "ORCHESTRATOR SERVICE" + (ansi rst) + "\n" +
|
||||
" provisioning orchestrator start - Start orchestrator\n" +
|
||||
" provisioning orchestrator status - Check status\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Setup help
|
||||
def help-setup []: nothing -> string {
|
||||
(
|
||||
(ansi magenta) + (ansi bo) + "SYSTEM SETUP & CONFIGURATION" + (ansi rst) + "\n\n" +
|
||||
"Initialize and configure the provisioning system.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "INITIAL SETUP" + (ansi rst) + "\n" +
|
||||
" provisioning setup system - Complete system setup wizard\n" +
|
||||
" Interactive TUI mode (default), auto-detect OS, setup platform services\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "WORKSPACE SETUP" + (ansi rst) + "\n" +
|
||||
" provisioning setup workspace <name> - Create new workspace\n" +
|
||||
" Initialize workspace structure, set active providers\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "PROVIDER SETUP" + (ansi rst) + "\n" +
|
||||
" provisioning setup provider <name> - Configure cloud provider\n" +
|
||||
" Supported: upcloud, aws, hetzner, local\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "PLATFORM SETUP" + (ansi rst) + "\n" +
|
||||
" provisioning setup platform - Setup platform services\n" +
|
||||
" Orchestrator, Control Center, KMS Service, MCP Server\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "SETUP MODES" + (ansi rst) + "\n" +
|
||||
" --interactive - Beautiful TUI wizard (default)\n" +
|
||||
" --config <file> - Load settings from TOML/YAML file\n" +
|
||||
" --defaults - Auto-detect and use sensible defaults\n\n" +
|
||||
|
||||
(ansi cyan) + "SETUP PHASES:" + (ansi rst) + "\n" +
|
||||
" 1. System Setup - Initialize OS-appropriate paths and services\n" +
|
||||
" 2. Workspace - Create infrastructure project workspace\n" +
|
||||
" 3. Providers - Register cloud providers with credentials\n" +
|
||||
" 4. Platform - Launch orchestration and control services\n" +
|
||||
" 5. Validation - Verify all components working\n\n" +
|
||||
|
||||
(ansi cyan) + "SECURITY:" + (ansi rst) + "\n" +
|
||||
" • RustyVault: Primary credentials storage (encrypt/decrypt at rest)\n" +
|
||||
" • SOPS/Age: Bootstrap encryption for RustyVault key only\n" +
|
||||
" • Cedar: Fine-grained access policies\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "QUICK START EXAMPLES" + (ansi rst) + "\n" +
|
||||
" provisioning setup system --interactive # TUI setup (recommended)\n" +
|
||||
" provisioning setup workspace myproject # Create workspace\n" +
|
||||
" provisioning setup provider upcloud # Configure provider\n" +
|
||||
" provisioning setup platform --mode solo # Setup services\n"
|
||||
(ansi red) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Authentication help
|
||||
def help-authentication []: nothing -> string {
|
||||
def help-authentication [] {
|
||||
let title = (get-help-string "help-authentication-title")
|
||||
let intro = (get-help-string "help-authentication-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "AUTHENTICATION AND SECURITY" + (ansi rst) + "\n\n" +
|
||||
"Manage user authentication, MFA, and security.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "LOGIN AND SESSIONS" + (ansi rst) + "\n" +
|
||||
" provisioning login - Login to system\n" +
|
||||
" provisioning logout - Logout from system\n"
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# MFA help
|
||||
def help-mfa []: nothing -> string {
|
||||
def help-mfa [] {
|
||||
let title = (get-help-string "help-mfa-title")
|
||||
let intro = (get-help-string "help-mfa-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "MULTI-FACTOR AUTHENTICATION" + (ansi rst) + "\n\n" +
|
||||
"Setup and manage MFA methods.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "TOTP (Time-based One-Time Password)" + (ansi rst) + "\n" +
|
||||
" provisioning mfa totp enroll - Enroll in TOTP\n" +
|
||||
" provisioning mfa totp verify <code> - Verify TOTP code\n"
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Plugins help
|
||||
def help-plugins []: nothing -> string {
|
||||
def help-plugins [] {
|
||||
let title = (get-help-string "help-plugins-title")
|
||||
let intro = (get-help-string "help-plugins-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "PLUGIN MANAGEMENT" + (ansi rst) + "\n\n" +
|
||||
"Install, configure, and manage Nushell plugins.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "PLUGIN COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning plugin list - List installed plugins\n" +
|
||||
" provisioning plugin install <name> - Install plugin\n"
|
||||
(ansi cyan) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Utilities help
|
||||
def help-utilities []: nothing -> string {
|
||||
def help-utilities [] {
|
||||
let title = (get-help-string "help-utilities-title")
|
||||
let intro = (get-help-string "help-utilities-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "UTILITIES & TOOLS" + (ansi rst) + "\n\n" +
|
||||
"Cache management, secrets, providers, and miscellaneous tools.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "CACHE COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning cache status - Show cache status and statistics\n" +
|
||||
" provisioning cache config show - Display all cache settings\n" +
|
||||
" provisioning cache config get <setting> - Get specific cache setting\n" +
|
||||
" provisioning cache config set <setting> <val> - Set cache setting\n" +
|
||||
" provisioning cache list [--type TYPE] - List cached items\n" +
|
||||
" provisioning cache clear [--type TYPE] - Clear cache\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "OTHER UTILITIES" + (ansi rst) + "\n" +
|
||||
" provisioning sops <file> - Edit encrypted file\n" +
|
||||
" provisioning encrypt <file> - Encrypt configuration\n" +
|
||||
" provisioning decrypt <file> - Decrypt configuration\n" +
|
||||
" provisioning providers list - List available providers\n" +
|
||||
" provisioning plugin list - List installed plugins\n" +
|
||||
" provisioning ssh <host> - Connect to server\n\n" +
|
||||
|
||||
(ansi cyan) + "Cache Features:" + (ansi rst) + "\n" +
|
||||
" • Intelligent TTL management (Nickel: 30m, SOPS: 15m, Final: 5m)\n" +
|
||||
" • 95-98% faster config loading\n" +
|
||||
" • SOPS cache with 0600 permissions\n" +
|
||||
" • Works without active workspace\n\n" +
|
||||
|
||||
(ansi cyan) + "Cache Configuration:" + (ansi rst) + "\n" +
|
||||
" provisioning cache config set ttl_nickel 3000 # Set Nickel TTL\n" +
|
||||
" provisioning cache config set enabled false # Disable cache\n"
|
||||
(ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Tools help
|
||||
def help-tools []: nothing -> string {
|
||||
def help-tools [] {
|
||||
let title = (get-help-string "help-tools-title")
|
||||
let intro = (get-help-string "help-tools-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "TOOLS & DEPENDENCIES" + (ansi rst) + "\n\n" +
|
||||
"Tool and dependency management for provisioning system.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "INSTALLATION" + (ansi rst) + "\n" +
|
||||
" provisioning tools install - Install all tools\n" +
|
||||
" provisioning tools install <tool> - Install specific tool\n" +
|
||||
" provisioning tools install --update - Force reinstall all tools\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "VERSION MANAGEMENT" + (ansi rst) + "\n" +
|
||||
" provisioning tools check - Check all tool versions\n" +
|
||||
" provisioning tools versions - Show configured versions\n" +
|
||||
" provisioning tools check-updates - Check for available updates\n" +
|
||||
" provisioning tools apply-updates - Apply configuration updates\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "TOOL INFORMATION" + (ansi rst) + "\n" +
|
||||
" provisioning tools show - Display tool information\n" +
|
||||
" provisioning tools show all - Show all tools\n" +
|
||||
" provisioning tools show provider - Show provider information\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "PINNING" + (ansi rst) + "\n" +
|
||||
" provisioning tools pin <tool> - Pin tool to current version\n" +
|
||||
" provisioning tools unpin <tool> - Unpin tool\n\n" +
|
||||
|
||||
(ansi cyan) + "Examples:" + (ansi rst) + "\n" +
|
||||
" provisioning tools check # Check all versions\n" +
|
||||
" provisioning tools check hcloud # Check hcloud status\n" +
|
||||
" provisioning tools check-updates # Check for updates\n" +
|
||||
" provisioning tools install # Install all tools\n"
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# VM help
|
||||
def help-vm []: nothing -> string {
|
||||
def help-vm [] {
|
||||
let title = (get-help-string "help-vm-title")
|
||||
let intro = (get-help-string "help-vm-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "VIRTUAL MACHINE OPERATIONS" + (ansi rst) + "\n\n" +
|
||||
"Manage virtual machines and hypervisors.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "VM COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning vm create <name> - Create VM\n" +
|
||||
" provisioning vm delete <name> - Delete VM\n"
|
||||
(ansi green) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Diagnostics help
|
||||
def help-diagnostics []: nothing -> string {
|
||||
def help-diagnostics [] {
|
||||
let title = (get-help-string "help-diagnostics-title")
|
||||
let intro = (get-help-string "help-diagnostics-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "DIAGNOSTICS AND HEALTH CHECKS" + (ansi rst) + "\n\n" +
|
||||
"Check system status and diagnose issues.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "STATUS COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning status - Overall system status\n" +
|
||||
" provisioning health - Health check\n"
|
||||
(ansi magenta) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Concepts help
|
||||
def help-concepts []: nothing -> string {
|
||||
def help-concepts [] {
|
||||
let title = (get-help-string "help-concepts-title")
|
||||
let intro = (get-help-string "help-concepts-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "PROVISIONING CONCEPTS" + (ansi rst) + "\n\n" +
|
||||
"Learn about the core concepts of the provisioning system.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "FUNDAMENTAL CONCEPTS" + (ansi rst) + "\n" +
|
||||
" workspace - A logical grouping of infrastructure\n" +
|
||||
" infrastructure - Configuration for a specific deployment\n" +
|
||||
" layer - Composable configuration units\n" +
|
||||
" taskserv - Infrastructure services (Kubernetes, etc.)\n"
|
||||
(ansi yellow) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Guides help
|
||||
def help-guides []: nothing -> string {
|
||||
def help-guides [] {
|
||||
let title = (get-help-string "help-guides-title")
|
||||
let intro = (get-help-string "help-guides-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "QUICK GUIDES AND CHEATSHEETS" + (ansi rst) + "\n\n" +
|
||||
"Step-by-step guides for common tasks.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "GETTING STARTED" + (ansi rst) + "\n" +
|
||||
" provisioning guide from-scratch - Deploy from scratch\n" +
|
||||
" provisioning guide quickstart - Quick reference\n" +
|
||||
" provisioning guide setup-system - Complete system setup guide\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "SETUP GUIDES" + (ansi rst) + "\n" +
|
||||
" provisioning guide setup-workspace - Create and configure workspaces\n" +
|
||||
" provisioning guide setup-providers - Configure cloud providers\n" +
|
||||
" provisioning guide setup-platform - Setup platform services\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "INFRASTRUCTURE MANAGEMENT" + (ansi rst) + "\n" +
|
||||
" provisioning guide update - Update existing infrastructure safely\n" +
|
||||
" provisioning guide customize - Customize with layers and templates\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "QUICK COMMANDS" + (ansi rst) + "\n" +
|
||||
" provisioning sc - Quick command reference (fastest)\n" +
|
||||
" provisioning guide list - Show all available guides\n"
|
||||
(ansi blue) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
# Integrations help
|
||||
def help-integrations []: nothing -> string {
|
||||
def help-integrations [] {
|
||||
let title = (get-help-string "help-integrations-title")
|
||||
let intro = (get-help-string "help-integrations-intro")
|
||||
let more_info = (get-help-string "help-more-info")
|
||||
(
|
||||
(ansi yellow) + (ansi bo) + "ECOSYSTEM AND INTEGRATIONS" + (ansi rst) + "\n\n" +
|
||||
"Integration with external systems and tools.\n\n" +
|
||||
|
||||
(ansi green) + (ansi bo) + "ECOSYSTEM COMPONENTS" + (ansi rst) + "\n" +
|
||||
" ProvCtl - Provisioning Control tool\n" +
|
||||
" Orchestrator - Workflow engine\n"
|
||||
(ansi cyan) + (ansi bo) + ($title) + (ansi rst) + "\n\n" +
|
||||
($intro) + "\n\n" +
|
||||
($more_info) + "\n"
|
||||
)
|
||||
}
|
||||
|
||||
@ -440,5 +579,3 @@ def main [...args: string] {
|
||||
let help_text = (provisioning-help $category)
|
||||
print $help_text
|
||||
}
|
||||
|
||||
# NOTE: No entry point needed - functions are called directly from bash script
|
||||
|
||||
322
nulib/kms/mod.nu
322
nulib/kms/mod.nu
@ -1,6 +1,320 @@
|
||||
#!/usr/bin/env nu
|
||||
const LOG_ANSI = {
|
||||
"CRITICAL": (ansi red_bold),
|
||||
"ERROR": (ansi red),
|
||||
"WARNING": (ansi yellow),
|
||||
"INFO": (ansi default),
|
||||
"DEBUG": (ansi default_dimmed)
|
||||
}
|
||||
|
||||
# KMS Service Module
|
||||
# Unified interface for Key Management Service operations
|
||||
export def log-ansi [] {$LOG_ANSI}
|
||||
|
||||
export use service.nu *
|
||||
const LOG_LEVEL = {
|
||||
"CRITICAL": 50,
|
||||
"ERROR": 40,
|
||||
"WARNING": 30,
|
||||
"INFO": 20,
|
||||
"DEBUG": 10
|
||||
}
|
||||
|
||||
export def log-level [] {$LOG_LEVEL}
|
||||
|
||||
const LOG_PREFIX = {
|
||||
"CRITICAL": "CRT",
|
||||
"ERROR": "ERR",
|
||||
"WARNING": "WRN",
|
||||
"INFO": "INF",
|
||||
"DEBUG": "DBG"
|
||||
}
|
||||
|
||||
export def log-prefix [] {$LOG_PREFIX}
|
||||
|
||||
const LOG_SHORT_PREFIX = {
|
||||
"CRITICAL": "C",
|
||||
"ERROR": "E",
|
||||
"WARNING": "W",
|
||||
"INFO": "I",
|
||||
"DEBUG": "D"
|
||||
}
|
||||
|
||||
export def log-short-prefix [] {$LOG_SHORT_PREFIX}
|
||||
|
||||
const LOG_FORMATS = {
|
||||
log: "%ANSI_START%%DATE%|%LEVEL%|%MSG%%ANSI_STOP%"
|
||||
date: "%Y-%m-%dT%H:%M:%S%.3f"
|
||||
}
|
||||
|
||||
export-env {
|
||||
$env.NU_LOG_FORMAT = $env.NU_LOG_FORMAT? | default $LOG_FORMATS.log
|
||||
$env.NU_LOG_DATE_FORMAT = $env.NU_LOG_DATE_FORMAT? | default $LOG_FORMATS.date
|
||||
}
|
||||
|
||||
const LOG_TYPES = {
|
||||
"CRITICAL": {
|
||||
"ansi": $LOG_ANSI.CRITICAL,
|
||||
"level": $LOG_LEVEL.CRITICAL,
|
||||
"prefix": $LOG_PREFIX.CRITICAL,
|
||||
"short_prefix": $LOG_SHORT_PREFIX.CRITICAL
|
||||
},
|
||||
"ERROR": {
|
||||
"ansi": $LOG_ANSI.ERROR,
|
||||
"level": $LOG_LEVEL.ERROR,
|
||||
"prefix": $LOG_PREFIX.ERROR,
|
||||
"short_prefix": $LOG_SHORT_PREFIX.ERROR
|
||||
},
|
||||
"WARNING": {
|
||||
"ansi": $LOG_ANSI.WARNING,
|
||||
"level": $LOG_LEVEL.WARNING,
|
||||
"prefix": $LOG_PREFIX.WARNING,
|
||||
"short_prefix": $LOG_SHORT_PREFIX.WARNING
|
||||
},
|
||||
"INFO": {
|
||||
"ansi": $LOG_ANSI.INFO,
|
||||
"level": $LOG_LEVEL.INFO,
|
||||
"prefix": $LOG_PREFIX.INFO,
|
||||
"short_prefix": $LOG_SHORT_PREFIX.INFO
|
||||
},
|
||||
"DEBUG": {
|
||||
"ansi": $LOG_ANSI.DEBUG,
|
||||
"level": $LOG_LEVEL.DEBUG,
|
||||
"prefix": $LOG_PREFIX.DEBUG,
|
||||
"short_prefix": $LOG_SHORT_PREFIX.DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
def parse-string-level [
|
||||
level: string
|
||||
] {
|
||||
let level = ($level | str upcase)
|
||||
|
||||
if $level in [$LOG_PREFIX.CRITICAL $LOG_SHORT_PREFIX.CRITICAL "CRIT" "CRITICAL"] {
|
||||
$LOG_LEVEL.CRITICAL
|
||||
} else if $level in [$LOG_PREFIX.ERROR $LOG_SHORT_PREFIX.ERROR "ERROR"] {
|
||||
$LOG_LEVEL.ERROR
|
||||
} else if $level in [$LOG_PREFIX.WARNING $LOG_SHORT_PREFIX.WARNING "WARN" "WARNING"] {
|
||||
$LOG_LEVEL.WARNING
|
||||
} else if $level in [$LOG_PREFIX.DEBUG $LOG_SHORT_PREFIX.DEBUG "DEBUG"] {
|
||||
$LOG_LEVEL.DEBUG
|
||||
} else {
|
||||
$LOG_LEVEL.INFO
|
||||
}
|
||||
}
|
||||
|
||||
def parse-int-level [
|
||||
level: int,
|
||||
--short (-s)
|
||||
] {
|
||||
if $level >= $LOG_LEVEL.CRITICAL {
|
||||
if $short {
|
||||
$LOG_SHORT_PREFIX.CRITICAL
|
||||
} else {
|
||||
$LOG_PREFIX.CRITICAL
|
||||
}
|
||||
} else if $level >= $LOG_LEVEL.ERROR {
|
||||
if $short {
|
||||
$LOG_SHORT_PREFIX.ERROR
|
||||
} else {
|
||||
$LOG_PREFIX.ERROR
|
||||
}
|
||||
} else if $level >= $LOG_LEVEL.WARNING {
|
||||
if $short {
|
||||
$LOG_SHORT_PREFIX.WARNING
|
||||
} else {
|
||||
$LOG_PREFIX.WARNING
|
||||
}
|
||||
} else if $level >= $LOG_LEVEL.INFO {
|
||||
if $short {
|
||||
$LOG_SHORT_PREFIX.INFO
|
||||
} else {
|
||||
$LOG_PREFIX.INFO
|
||||
}
|
||||
} else {
|
||||
if $short {
|
||||
$LOG_SHORT_PREFIX.DEBUG
|
||||
} else {
|
||||
$LOG_PREFIX.DEBUG
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def current-log-level [] {
|
||||
let env_level = ($env.NU_LOG_LEVEL? | default $LOG_LEVEL.INFO)
|
||||
|
||||
let result = (do { $env_level | into int } | complete)
|
||||
if $result.exit_code == 0 { $result.stdout } else { parse-string-level $env_level }
|
||||
}
|
||||
|
||||
def now [] {
|
||||
date now | format date ($env.NU_LOG_DATE_FORMAT? | default $LOG_FORMATS.date)
|
||||
}
|
||||
|
||||
def handle-log [
|
||||
message: string,
|
||||
formatting: record,
|
||||
format_string: string,
|
||||
short: bool
|
||||
] {
|
||||
let log_format = $format_string | default -e $env.NU_LOG_FORMAT? | default $LOG_FORMATS.log
|
||||
|
||||
let prefix = if $short {
|
||||
$formatting.short_prefix
|
||||
} else {
|
||||
$formatting.prefix
|
||||
}
|
||||
|
||||
custom $message $log_format $formatting.level --level-prefix $prefix --ansi $formatting.ansi
|
||||
}
|
||||
|
||||
# Logging module
|
||||
#
|
||||
# Log formatting placeholders:
|
||||
# - %MSG%: message to be logged
|
||||
# - %DATE%: date of log
|
||||
# - %LEVEL%: string prefix for the log level
|
||||
# - %ANSI_START%: ansi formatting
|
||||
# - %ANSI_STOP%: literally (ansi reset)
|
||||
#
|
||||
# Note: All placeholders are optional, so "" is still a valid format
|
||||
#
|
||||
# Example: $"%ANSI_START%%DATE%|%LEVEL%|(ansi u)%MSG%%ANSI_STOP%"
|
||||
export def main [] {}
|
||||
|
||||
# Log a critical message
|
||||
export def critical [
|
||||
message: string, # A message
|
||||
--short (-s) # Whether to use a short prefix
|
||||
--format (-f): string # A format (for further reference: help std log)
|
||||
] {
|
||||
let format = $format | default ""
|
||||
handle-log $message ($LOG_TYPES.CRITICAL) $format $short
|
||||
}
|
||||
|
||||
# Log an error message
|
||||
export def error [
|
||||
message: string, # A message
|
||||
--short (-s) # Whether to use a short prefix
|
||||
--format (-f): string # A format (for further reference: help std log)
|
||||
] {
|
||||
let format = $format | default ""
|
||||
handle-log $message ($LOG_TYPES.ERROR) $format $short
|
||||
}
|
||||
|
||||
# Log a warning message
|
||||
export def warning [
|
||||
message: string, # A message
|
||||
--short (-s) # Whether to use a short prefix
|
||||
--format (-f): string # A format (for further reference: help std log)
|
||||
] {
|
||||
let format = $format | default ""
|
||||
handle-log $message ($LOG_TYPES.WARNING) $format $short
|
||||
}
|
||||
|
||||
# Log an info message
|
||||
export def info [
|
||||
message: string, # A message
|
||||
--short (-s) # Whether to use a short prefix
|
||||
--format (-f): string # A format (for further reference: help std log)
|
||||
] {
|
||||
let format = $format | default ""
|
||||
handle-log $message ($LOG_TYPES.INFO) $format $short
|
||||
}
|
||||
|
||||
# Log a debug message
|
||||
export def debug [
|
||||
message: string, # A message
|
||||
--short (-s) # Whether to use a short prefix
|
||||
--format (-f): string # A format (for further reference: help std log)
|
||||
] {
|
||||
let format = $format | default ""
|
||||
handle-log $message ($LOG_TYPES.DEBUG) $format $short
|
||||
}
|
||||
|
||||
def log-level-deduction-error [
|
||||
type: string
|
||||
span: record<start: int, end: int>
|
||||
log_level: int
|
||||
] {
|
||||
error make {
|
||||
msg: $"(ansi red_bold)Cannot deduce ($type) for given log level: ($log_level).(ansi reset)"
|
||||
label: {
|
||||
text: ([
|
||||
"Invalid log level."
|
||||
$" Available log levels in log-level:"
|
||||
($LOG_LEVEL | to text | lines | each {|it| $" ($it)" } | to text)
|
||||
] | str join "\n")
|
||||
span: $span
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Log a message with a specific format and verbosity level, with either configurable or auto-deduced %LEVEL% and %ANSI_START% placeholder extensions
|
||||
export def custom [
|
||||
message: string, # A message
|
||||
format: string, # A format (for further reference: help std log)
|
||||
log_level: int # A log level (has to be one of the log-level values for correct ansi/prefix deduction)
|
||||
--level-prefix (-p): string # %LEVEL% placeholder extension
|
||||
--ansi (-a): string # %ANSI_START% placeholder extension
|
||||
] {
|
||||
if (current-log-level) > ($log_level) {
|
||||
return
|
||||
}
|
||||
|
||||
let valid_levels_for_defaulting = [
|
||||
$LOG_LEVEL.CRITICAL
|
||||
$LOG_LEVEL.ERROR
|
||||
$LOG_LEVEL.WARNING
|
||||
$LOG_LEVEL.INFO
|
||||
$LOG_LEVEL.DEBUG
|
||||
]
|
||||
|
||||
let prefix = if ($level_prefix | is-empty) {
|
||||
if ($log_level not-in $valid_levels_for_defaulting) {
|
||||
log-level-deduction-error "log level prefix" (metadata $log_level).span $log_level
|
||||
}
|
||||
|
||||
parse-int-level $log_level
|
||||
|
||||
} else {
|
||||
$level_prefix
|
||||
}
|
||||
|
||||
let use_color = ($env.config?.use_ansi_coloring? | $in != false)
|
||||
let ansi = if not $use_color {
|
||||
""
|
||||
} else if ($ansi | is-empty) {
|
||||
if ($log_level not-in $valid_levels_for_defaulting) {
|
||||
log-level-deduction-error "ansi" (metadata $log_level).span $log_level
|
||||
}
|
||||
|
||||
(
|
||||
$LOG_TYPES
|
||||
| values
|
||||
| each {|record|
|
||||
if ($record.level == $log_level) {
|
||||
$record.ansi
|
||||
}
|
||||
} | first
|
||||
)
|
||||
} else {
|
||||
$ansi
|
||||
}
|
||||
|
||||
print --stderr (
|
||||
$format
|
||||
| str replace --all "%MSG%" $message
|
||||
| str replace --all "%DATE%" (now)
|
||||
| str replace --all "%LEVEL%" $prefix
|
||||
| str replace --all "%ANSI_START%" $ansi
|
||||
| str replace --all "%ANSI_STOP%" (ansi reset)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
def "nu-complete log-level" [] {
|
||||
$LOG_LEVEL | transpose description value
|
||||
}
|
||||
|
||||
# Change logging level
|
||||
export def --env set-level [level: int@"nu-complete log-level"] {
|
||||
# Keep it as a string so it can be passed to child processes
|
||||
$env.NU_LOG_LEVEL = $level | into string
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
# Get user config path (centralized location)
|
||||
# Rule 2: Single purpose function
|
||||
# Cross-platform support (macOS, Linux, Windows)
|
||||
def get-user-config-path []: nothing -> string {
|
||||
def get-user-config-path [] {
|
||||
let home = $env.HOME
|
||||
let os_name = (uname | get operating-system | str downcase)
|
||||
|
||||
@ -21,7 +21,7 @@ def get-user-config-path []: nothing -> string {
|
||||
# List all registered workspaces
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
# Rule 2: Single purpose - only list workspaces
|
||||
export def workspace-list []: nothing -> list {
|
||||
export def workspace-list [] {
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Rule 4: Early return if config doesn't exist
|
||||
@ -60,7 +60,7 @@ export def workspace-list []: nothing -> list {
|
||||
|
||||
# Get active workspace name
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
export def workspace-active []: nothing -> string {
|
||||
export def workspace-active [] {
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Rule 4: Early return
|
||||
@ -78,7 +78,7 @@ export def workspace-active []: nothing -> string {
|
||||
|
||||
# Get workspace info by name
|
||||
# Rule 1: Explicit types, Rule 4: Early returns
|
||||
export def workspace-info [name: string]: nothing -> record {
|
||||
export def workspace-info [name: string] {
|
||||
let user_config = (get-user-config-path)
|
||||
|
||||
# Rule 4: Early return if config doesn't exist
|
||||
@ -111,7 +111,7 @@ export def workspace-info [name: string]: nothing -> record {
|
||||
|
||||
# Quick status check (orchestrator health + active workspace)
|
||||
# Rule 1: Explicit types, Rule 13: Appropriate error handling
|
||||
export def status-quick []: nothing -> record {
|
||||
export def status-quick [] {
|
||||
# Direct HTTP check (no bootstrap overhead)
|
||||
# Rule 13: Use try-catch for network operations
|
||||
let orch_health = (try {
|
||||
@ -138,7 +138,7 @@ export def status-quick []: nothing -> record {
|
||||
|
||||
# Display essential environment variables
|
||||
# Rule 1: Explicit types, Rule 8: Pure function (read-only)
|
||||
export def env-quick []: nothing -> record {
|
||||
export def env-quick [] {
|
||||
# Rule 8: No side effects, just reading env vars
|
||||
{
|
||||
PROVISIONING_ROOT: ($env.PROVISIONING_ROOT? | default "not set")
|
||||
@ -151,7 +151,7 @@ export def env-quick []: nothing -> record {
|
||||
|
||||
# Show quick help for fast-path commands
|
||||
# Rule 1: Explicit types, Rule 8: Pure function
|
||||
export def quick-help []: nothing -> string {
|
||||
export def quick-help [] {
|
||||
"Provisioning CLI - Fast Path Commands
|
||||
|
||||
Quick Commands (< 100ms):
|
||||
|
||||
@ -347,7 +347,7 @@ This launches an interactive session that asks specific questions to build optim
|
||||
|
||||
### 🏗️ **Module Structure**
|
||||
|
||||
```plaintext
|
||||
```text
|
||||
ai/
|
||||
├── lib.nu # Core AI functionality and API integration
|
||||
├── templates.nu # Nickel template generation functions
|
||||
|
||||
24
nulib/lib_provisioning/cache/cache_manager.nu
vendored
24
nulib/lib_provisioning/cache/cache_manager.nu
vendored
@ -7,7 +7,7 @@ use grace_checker.nu is-cache-valid?
|
||||
# Get version with progressive cache hierarchy
|
||||
export def get-cached-version [
|
||||
component: string # Component name (e.g., kubernetes, containerd)
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Cache hierarchy: infra -> provisioning -> source
|
||||
|
||||
# 1. Try infra cache first (project-specific)
|
||||
@ -42,7 +42,7 @@ export def get-cached-version [
|
||||
}
|
||||
|
||||
# Get version from infra cache
|
||||
def get-infra-cache [component: string]: nothing -> string {
|
||||
def get-infra-cache [component: string] {
|
||||
let cache_path = (get-infra-cache-path)
|
||||
let cache_file = ($cache_path | path join "versions.json")
|
||||
|
||||
@ -56,12 +56,14 @@ def get-infra-cache [component: string]: nothing -> string {
|
||||
}
|
||||
|
||||
let cache_data = ($result.stdout | from json)
|
||||
let version_data = ($cache_data | try { get $component } catch { {}) }
|
||||
($version_data | try { get current } catch { "") }
|
||||
let version_result = (do { $cache_data | get $component } | complete)
|
||||
let version_data = if $version_result.exit_code == 0 { $version_result.stdout } else { {} }
|
||||
let current_result = (do { $version_data | get current } | complete)
|
||||
if $current_result.exit_code == 0 { $current_result.stdout } else { "" }
|
||||
}
|
||||
|
||||
# Get version from provisioning cache
|
||||
def get-provisioning-cache [component: string]: nothing -> string {
|
||||
def get-provisioning-cache [component: string] {
|
||||
let cache_path = (get-provisioning-cache-path)
|
||||
let cache_file = ($cache_path | path join "versions.json")
|
||||
|
||||
@ -75,8 +77,10 @@ def get-provisioning-cache [component: string]: nothing -> string {
|
||||
}
|
||||
|
||||
let cache_data = ($result.stdout | from json)
|
||||
let version_data = ($cache_data | try { get $component } catch { {}) }
|
||||
($version_data | try { get current } catch { "") }
|
||||
let version_result = (do { $cache_data | get $component } | complete)
|
||||
let version_data = if $version_result.exit_code == 0 { $version_result.stdout } else { {} }
|
||||
let current_result = (do { $version_data | get current } | complete)
|
||||
if $current_result.exit_code == 0 { $current_result.stdout } else { "" }
|
||||
}
|
||||
|
||||
# Cache version data
|
||||
@ -117,7 +121,7 @@ export def cache-version [
|
||||
}
|
||||
|
||||
# Get cache paths from config
|
||||
export def get-infra-cache-path []: nothing -> string {
|
||||
export def get-infra-cache-path [] {
|
||||
use ../config/accessor.nu config-get
|
||||
let infra_path = (config-get "paths.infra" "")
|
||||
let current_infra = (config-get "infra.current" "default")
|
||||
@ -129,12 +133,12 @@ export def get-infra-cache-path []: nothing -> string {
|
||||
$infra_path | path join $current_infra "cache"
|
||||
}
|
||||
|
||||
export def get-provisioning-cache-path []: nothing -> string {
|
||||
export def get-provisioning-cache-path [] {
|
||||
use ../config/accessor.nu config-get
|
||||
config-get "cache.path" ".cache/versions"
|
||||
}
|
||||
|
||||
def get-default-grace-period []: nothing -> int {
|
||||
def get-default-grace-period [] {
|
||||
use ../config/accessor.nu config-get
|
||||
config-get "cache.grace_period" 86400
|
||||
}
|
||||
|
||||
26
nulib/lib_provisioning/cache/grace_checker.nu
vendored
26
nulib/lib_provisioning/cache/grace_checker.nu
vendored
@ -5,7 +5,7 @@
|
||||
export def is-cache-valid? [
|
||||
component: string # Component name
|
||||
cache_type: string # "infra" or "provisioning"
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let cache_path = if $cache_type == "infra" {
|
||||
get-infra-cache-path
|
||||
} else {
|
||||
@ -24,14 +24,17 @@ export def is-cache-valid? [
|
||||
}
|
||||
|
||||
let cache_data = ($result.stdout | from json)
|
||||
let version_data = ($cache_data | try { get $component } catch { {}) }
|
||||
let vd_result = (do { $cache_data | get $component } | complete)
|
||||
let version_data = if $vd_result.exit_code == 0 { $vd_result.stdout } else { {} }
|
||||
|
||||
if ($version_data | is-empty) {
|
||||
return false
|
||||
}
|
||||
|
||||
let cached_at = ($version_data | try { get cached_at } catch { "") }
|
||||
let grace_period = ($version_data | try { get grace_period } catch { (get-default-grace-period)) }
|
||||
let ca_result = (do { $version_data | get cached_at } | complete)
|
||||
let cached_at = if $ca_result.exit_code == 0 { $ca_result.stdout } else { "" }
|
||||
let gp_result = (do { $version_data | get grace_period } | complete)
|
||||
let grace_period = if $gp_result.exit_code == 0 { $gp_result.stdout } else { (get-default-grace-period) }
|
||||
|
||||
if ($cached_at | is-empty) {
|
||||
return false
|
||||
@ -54,7 +57,7 @@ export def is-cache-valid? [
|
||||
# Get expired cache entries
|
||||
export def get-expired-entries [
|
||||
cache_type: string # "infra" or "provisioning"
|
||||
]: nothing -> list<string> {
|
||||
] {
|
||||
let cache_path = if $cache_type == "infra" {
|
||||
get-infra-cache-path
|
||||
} else {
|
||||
@ -80,7 +83,7 @@ export def get-expired-entries [
|
||||
}
|
||||
|
||||
# Get components that need update check (check_latest = true and expired)
|
||||
export def get-components-needing-update []: nothing -> list<string> {
|
||||
export def get-components-needing-update [] {
|
||||
let components = []
|
||||
|
||||
# Check infra cache
|
||||
@ -98,7 +101,7 @@ export def get-components-needing-update []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Get components with check_latest = true
|
||||
def get-check-latest-components [cache_type: string]: nothing -> list<string> {
|
||||
def get-check-latest-components [cache_type: string] {
|
||||
let cache_path = if $cache_type == "infra" {
|
||||
get-infra-cache-path
|
||||
} else {
|
||||
@ -120,7 +123,8 @@ def get-check-latest-components [cache_type: string]: nothing -> list<string> {
|
||||
|
||||
$cache_data | columns | where { |component|
|
||||
let comp_data = ($cache_data | get $component)
|
||||
($comp_data | try { get check_latest } catch { false) }
|
||||
let cl_result = (do { $comp_data | get check_latest } | complete)
|
||||
if $cl_result.exit_code == 0 { $cl_result.stdout } else { false }
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +154,7 @@ export def invalidate-cache-entry [
|
||||
}
|
||||
|
||||
# Helper functions (same as in cache_manager.nu)
|
||||
def get-infra-cache-path []: nothing -> string {
|
||||
def get-infra-cache-path [] {
|
||||
use ../config/accessor.nu config-get
|
||||
let infra_path = (config-get "paths.infra" "")
|
||||
let current_infra = (config-get "infra.current" "default")
|
||||
@ -162,12 +166,12 @@ def get-infra-cache-path []: nothing -> string {
|
||||
$infra_path | path join $current_infra "cache"
|
||||
}
|
||||
|
||||
def get-provisioning-cache-path []: nothing -> string {
|
||||
def get-provisioning-cache-path [] {
|
||||
use ../config/accessor.nu config-get
|
||||
config-get "cache.path" ".cache/versions"
|
||||
}
|
||||
|
||||
def get-default-grace-period []: nothing -> int {
|
||||
def get-default-grace-period [] {
|
||||
use ../config/accessor.nu config-get
|
||||
config-get "cache.grace_period" 86400
|
||||
}
|
||||
|
||||
43
nulib/lib_provisioning/cache/version_loader.nu
vendored
43
nulib/lib_provisioning/cache/version_loader.nu
vendored
@ -4,7 +4,7 @@
|
||||
# Load version from source (Nickel files)
|
||||
export def load-version-from-source [
|
||||
component: string # Component name
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Try different source locations
|
||||
let taskserv_version = (load-taskserv-version $component)
|
||||
if ($taskserv_version | is-not-empty) {
|
||||
@ -25,7 +25,7 @@ export def load-version-from-source [
|
||||
}
|
||||
|
||||
# Load taskserv version from version.ncl files
|
||||
def load-taskserv-version [component: string]: nothing -> string {
|
||||
def load-taskserv-version [component: string] {
|
||||
# Find version.ncl file for component
|
||||
let version_files = [
|
||||
$"taskservs/($component)/nickel/version.ncl"
|
||||
@ -46,7 +46,7 @@ def load-taskserv-version [component: string]: nothing -> string {
|
||||
}
|
||||
|
||||
# Load core tool version
|
||||
def load-core-version [component: string]: nothing -> string {
|
||||
def load-core-version [component: string] {
|
||||
let core_file = "core/versions.ncl"
|
||||
|
||||
if ($core_file | path exists) {
|
||||
@ -60,7 +60,7 @@ def load-core-version [component: string]: nothing -> string {
|
||||
}
|
||||
|
||||
# Load provider tool version
|
||||
def load-provider-version [component: string]: nothing -> string {
|
||||
def load-provider-version [component: string] {
|
||||
# Check provider directories
|
||||
let providers = ["aws", "upcloud", "local"]
|
||||
|
||||
@ -84,7 +84,7 @@ def load-provider-version [component: string]: nothing -> string {
|
||||
}
|
||||
|
||||
# Extract version from Nickel file (taskserv format)
|
||||
def extract-version-from-nickel [file: string, component: string]: nothing -> string {
|
||||
def extract-version-from-nickel [file: string, component: string] {
|
||||
let decl_result = (^nickel $file | complete)
|
||||
|
||||
if $decl_result.exit_code != 0 {
|
||||
@ -110,17 +110,20 @@ def extract-version-from-nickel [file: string, component: string]: nothing -> st
|
||||
]
|
||||
|
||||
for key in $version_keys {
|
||||
let version_data = ($result | try { get $key } catch { {}) }
|
||||
let lookup_result = (do { $result | get $key } | complete)
|
||||
let version_data = if $lookup_result.exit_code == 0 { $lookup_result.stdout } else { {} }
|
||||
|
||||
if ($version_data | is-not-empty) {
|
||||
# Try TaskservVersion format first
|
||||
let current_version = ($version_data | try { get version.current } catch { "") }
|
||||
let cv_result = (do { $version_data | get version.current } | complete)
|
||||
let current_version = if $cv_result.exit_code == 0 { $cv_result.stdout } else { "" }
|
||||
if ($current_version | is-not-empty) {
|
||||
return $current_version
|
||||
}
|
||||
|
||||
# Try simple format
|
||||
let simple_version = ($version_data | try { get current } catch { "") }
|
||||
let sv_result = (do { $version_data | get current } | complete)
|
||||
let simple_version = if $sv_result.exit_code == 0 { $sv_result.stdout } else { "" }
|
||||
if ($simple_version | is-not-empty) {
|
||||
return $simple_version
|
||||
}
|
||||
@ -136,7 +139,7 @@ def extract-version-from-nickel [file: string, component: string]: nothing -> st
|
||||
}
|
||||
|
||||
# Extract version from core versions.ncl file
|
||||
def extract-core-version-from-nickel [file: string, component: string]: nothing -> string {
|
||||
def extract-core-version-from-nickel [file: string, component: string] {
|
||||
let decl_result = (^nickel $file | complete)
|
||||
|
||||
if $decl_result.exit_code != 0 {
|
||||
@ -155,12 +158,14 @@ def extract-core-version-from-nickel [file: string, component: string]: nothing
|
||||
let result = $parse_result.stdout
|
||||
|
||||
# Look for component in core_versions array or individual variables
|
||||
let core_versions = ($result | try { get core_versions } catch { []) }
|
||||
let cv_result = (do { $result | get core_versions } | complete)
|
||||
let core_versions = if $cv_result.exit_code == 0 { $cv_result.stdout } else { [] }
|
||||
|
||||
if ($core_versions | is-not-empty) {
|
||||
# Array format
|
||||
let component_data = ($core_versions | where name == $component | first | default {})
|
||||
let version = ($component_data | try { get version.current } catch { "") }
|
||||
let vc_result = (do { $component_data | get version.current } | complete)
|
||||
let version = if $vc_result.exit_code == 0 { $vc_result.stdout } else { "" }
|
||||
if ($version | is-not-empty) {
|
||||
return $version
|
||||
}
|
||||
@ -173,9 +178,11 @@ def extract-core-version-from-nickel [file: string, component: string]: nothing
|
||||
]
|
||||
|
||||
for pattern in $var_patterns {
|
||||
let version_data = ($result | try { get $pattern } catch { {}) }
|
||||
let vd_result = (do { $result | get $pattern } | complete)
|
||||
let version_data = if $vd_result.exit_code == 0 { $vd_result.stdout } else { {} }
|
||||
if ($version_data | is-not-empty) {
|
||||
let current = ($version_data | try { get current } catch { "") }
|
||||
let curr_result = (do { $version_data | get current } | complete)
|
||||
let current = if $curr_result.exit_code == 0 { $curr_result.stdout } else { "" }
|
||||
if ($current | is-not-empty) {
|
||||
return $current
|
||||
}
|
||||
@ -188,7 +195,7 @@ def extract-core-version-from-nickel [file: string, component: string]: nothing
|
||||
# Batch load multiple versions (for efficiency)
|
||||
export def batch-load-versions [
|
||||
components: list<string> # List of component names
|
||||
]: nothing -> record {
|
||||
] {
|
||||
mut results = {}
|
||||
|
||||
for component in $components {
|
||||
@ -202,7 +209,7 @@ export def batch-load-versions [
|
||||
}
|
||||
|
||||
# Get all available components
|
||||
export def get-all-components []: nothing -> list<string> {
|
||||
export def get-all-components [] {
|
||||
let taskservs = (get-taskserv-components)
|
||||
let core_tools = (get-core-components)
|
||||
let providers = (get-provider-components)
|
||||
@ -211,7 +218,7 @@ export def get-all-components []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Get taskserv components
|
||||
def get-taskserv-components []: nothing -> list<string> {
|
||||
def get-taskserv-components [] {
|
||||
let result = (do { glob "taskservs/*/nickel/version.ncl" } | complete)
|
||||
if $result.exit_code != 0 {
|
||||
return []
|
||||
@ -223,7 +230,7 @@ def get-taskserv-components []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Get core components
|
||||
def get-core-components []: nothing -> list<string> {
|
||||
def get-core-components [] {
|
||||
if not ("core/versions.ncl" | path exists) {
|
||||
return []
|
||||
}
|
||||
@ -245,7 +252,7 @@ def get-core-components []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Get provider components (placeholder)
|
||||
def get-provider-components []: nothing -> list<string> {
|
||||
def get-provider-components [] {
|
||||
# TODO: Implement provider component discovery
|
||||
[]
|
||||
}
|
||||
|
||||
@ -6,13 +6,13 @@ use ../sops *
|
||||
|
||||
export def log_debug [
|
||||
msg: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
use std
|
||||
std log debug $msg
|
||||
# std assert (1 == 1)
|
||||
}
|
||||
export def check_env [
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let vars_path = (get-provisioning-vars)
|
||||
if ($vars_path | is-empty) {
|
||||
_print $"🛑 Error no values found for (_ansi red_bold)PROVISIONING_VARS(_ansi reset)"
|
||||
@ -47,7 +47,7 @@ export def sops_cmd [
|
||||
source: string
|
||||
target?: string
|
||||
--error_exit # error on exit
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let sops_key = (find-sops-key)
|
||||
if ($sops_key | is-empty) {
|
||||
$env.CURRENT_INFRA_PATH = ((get-provisioning-infra-path) | path join (get-workspace-path | path basename))
|
||||
@ -62,7 +62,7 @@ export def sops_cmd [
|
||||
}
|
||||
|
||||
export def load_defs [
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let vars_path = (get-provisioning-vars)
|
||||
if not ($vars_path | path exists) {
|
||||
_print $"🛑 Error file (_ansi red_bold)($vars_path)(_ansi reset) not found"
|
||||
|
||||
865
nulib/lib_provisioning/config/accessor_generated.nu
Normal file
865
nulib/lib_provisioning/config/accessor_generated.nu
Normal file
@ -0,0 +1,865 @@
|
||||
# Configuration Accessor Functions
|
||||
# Generated from Nickel schema: /Users/Akasha/project-provisioning/provisioning/schemas/config/settings/main.ncl
|
||||
# DO NOT EDIT - Generated by accessor_generator.nu v1.0.0
|
||||
#
|
||||
# Generator version: 1.0.0
|
||||
# Generated: 2026-01-13T13:49:23Z
|
||||
# Schema: /Users/Akasha/project-provisioning/provisioning/schemas/config/settings/main.ncl
|
||||
# Schema Hash: e129e50bba0128e066412eb63b12f6fd0f955d43133e1826dd5dc9405b8a9647
|
||||
# Accessor Count: 76
|
||||
#
|
||||
# This file contains 76 accessor functions automatically generated
|
||||
# from the Nickel schema. Each function provides type-safe access to a
|
||||
# configuration value with proper defaults.
|
||||
#
|
||||
# NUSHELL COMPLIANCE:
|
||||
# - Rule 3: No mutable variables, uses reduce fold
|
||||
# - Rule 5: Uses do-complete error handling pattern
|
||||
# - Rule 8: Uses is-not-empty and each
|
||||
# - Rule 9: Boolean flags without type annotations
|
||||
# - Rule 11: All functions are exported
|
||||
# - Rule 15: No parameterized types
|
||||
#
|
||||
# NICKEL COMPLIANCE:
|
||||
# - Schema-first design with all fields from schema
|
||||
# - Design by contract via schema validation
|
||||
# - JSON output validation for schema types
|
||||
|
||||
use ./accessor.nu config-get
|
||||
use ./accessor.nu get-config
|
||||
|
||||
export def get-DefaultAIProvider-enable_query_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.enable_query_ai" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-enable_template_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.enable_template_ai" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-enable_webhook_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.enable_webhook_ai" false --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-enabled [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.enabled" false --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-max_tokens [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.max_tokens" 2048 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-provider [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.provider" "openai" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-temperature [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.temperature" 0.3 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultAIProvider-timeout [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultAIProvider.timeout" 30 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultKmsConfig-auth_method [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultKmsConfig.auth_method" "certificate" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultKmsConfig-server_url [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultKmsConfig.server_url" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultKmsConfig-timeout [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultKmsConfig.timeout" 30 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultKmsConfig-verify_ssl [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultKmsConfig.verify_ssl" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultRunSet-inventory_file [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultRunSet.inventory_file" "./inventory.yaml" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultRunSet-output_format [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultRunSet.output_format" "human" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultRunSet-output_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultRunSet.output_path" "tmp/NOW-deploy" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultRunSet-use_time [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultRunSet.use_time" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultRunSet-wait [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultRunSet.wait" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSecretProvider-provider [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSecretProvider.provider" "sops" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-cluster_admin_host [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.cluster_admin_host" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-cluster_admin_port [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.cluster_admin_port" 22 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-cluster_admin_user [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.cluster_admin_user" "root" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-clusters_paths [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.clusters_paths" null --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-clusters_save_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.clusters_save_path" "/${main_name}/clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-created_clusters_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.created_clusters_dirpath" "./tmp/NOW_clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-created_taskservs_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.created_taskservs_dirpath" "./tmp/NOW_deployment" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-defaults_provs_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.defaults_provs_dirpath" "./defs" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-defaults_provs_suffix [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.defaults_provs_suffix" "_defaults.k" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-main_name [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.main_name" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-main_title [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.main_title" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-prov_clusters_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.prov_clusters_path" "./clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-prov_data_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.prov_data_dirpath" "./data" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-prov_data_suffix [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.prov_data_suffix" "_settings.k" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-prov_local_bin_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.prov_local_bin_path" "./bin" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-prov_resources_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.prov_resources_path" "./resources" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-servers_paths [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.servers_paths" null --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-servers_wait_started [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.servers_wait_started" 27 --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSettings-settings_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSettings.settings_path" "./settings.yaml" --config $cfg
|
||||
}
|
||||
|
||||
export def get-DefaultSopsConfig-use_age [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "DefaultSopsConfig.use_age" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-enable_query_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.enable_query_ai" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-enable_template_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.enable_template_ai" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-enable_webhook_ai [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.enable_webhook_ai" false --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-enabled [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.enabled" false --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-max_tokens [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.max_tokens" 2048 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-provider [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.provider" "openai" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-temperature [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.temperature" 0.3 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-ai_provider-timeout [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.ai_provider.timeout" 30 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-kms_config-auth_method [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.kms_config.auth_method" "certificate" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-kms_config-server_url [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.kms_config.server_url" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-kms_config-timeout [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.kms_config.timeout" 30 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-kms_config-verify_ssl [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.kms_config.verify_ssl" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-run_set-inventory_file [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.run_set.inventory_file" "./inventory.yaml" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-run_set-output_format [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.run_set.output_format" "human" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-run_set-output_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.run_set.output_path" "tmp/NOW-deploy" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-run_set-use_time [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.run_set.use_time" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-run_set-wait [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.run_set.wait" true --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-secret_provider-provider [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.secret_provider.provider" "sops" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-cluster_admin_host [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.cluster_admin_host" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-cluster_admin_port [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.cluster_admin_port" 22 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-cluster_admin_user [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.cluster_admin_user" "root" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-clusters_paths [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.clusters_paths" null --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-clusters_save_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.clusters_save_path" "/${main_name}/clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-created_clusters_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.created_clusters_dirpath" "./tmp/NOW_clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-created_taskservs_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.created_taskservs_dirpath" "./tmp/NOW_deployment" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-defaults_provs_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.defaults_provs_dirpath" "./defs" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-defaults_provs_suffix [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.defaults_provs_suffix" "_defaults.k" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-main_name [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.main_name" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-main_title [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.main_title" "" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-prov_clusters_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.prov_clusters_path" "./clusters" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-prov_data_dirpath [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.prov_data_dirpath" "./data" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-prov_data_suffix [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.prov_data_suffix" "_settings.k" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-prov_local_bin_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.prov_local_bin_path" "./bin" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-prov_resources_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.prov_resources_path" "./resources" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-servers_paths [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.servers_paths" null --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-servers_wait_started [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.servers_wait_started" 27 --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-settings-settings_path [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.settings.settings_path" "./settings.yaml" --config $cfg
|
||||
}
|
||||
|
||||
export def get-defaults-sops_config-use_age [
|
||||
--cfg_input: any = null
|
||||
] {
|
||||
let cfg = if ($cfg_input | is-not-empty) {
|
||||
$cfg_input
|
||||
} else {
|
||||
get-config
|
||||
}
|
||||
config-get "defaults.sops_config.use_age" true --config $cfg
|
||||
}
|
||||
@ -11,7 +11,7 @@ use accessor.nu *
|
||||
# Detect if a config file is encrypted
|
||||
export def is-encrypted-config [
|
||||
file_path: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
if not ($file_path | path exists) {
|
||||
return false
|
||||
}
|
||||
@ -24,7 +24,7 @@ export def is-encrypted-config [
|
||||
export def load-encrypted-config [
|
||||
file_path: string
|
||||
--debug = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
if not ($file_path | path exists) {
|
||||
error make {
|
||||
msg: $"Configuration file not found: ($file_path)"
|
||||
@ -69,7 +69,7 @@ export def load-encrypted-config [
|
||||
export def decrypt-config-memory [
|
||||
file_path: string
|
||||
--debug = false
|
||||
]: nothing -> string {
|
||||
] {
|
||||
if not (is-encrypted-config $file_path) {
|
||||
error make {
|
||||
msg: $"File is not encrypted: ($file_path)"
|
||||
@ -133,7 +133,7 @@ export def encrypt-config [
|
||||
--kms: string = "age" # age, rustyvault, aws-kms, vault, cosmian
|
||||
--in-place = false
|
||||
--debug = false
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if not ($source_path | path exists) {
|
||||
error make {
|
||||
msg: $"Source file not found: ($source_path)"
|
||||
@ -257,7 +257,7 @@ export def decrypt-config [
|
||||
output_path?: string
|
||||
--in-place = false
|
||||
--debug = false
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if not ($source_path | path exists) {
|
||||
error make {
|
||||
msg: $"Source file not found: ($source_path)"
|
||||
@ -305,7 +305,7 @@ export def edit-encrypted-config [
|
||||
file_path: string
|
||||
--editor: string = ""
|
||||
--debug = false
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if not ($file_path | path exists) {
|
||||
error make {
|
||||
msg: $"File not found: ($file_path)"
|
||||
@ -343,7 +343,7 @@ export def rotate-encryption-keys [
|
||||
file_path: string
|
||||
new_key_id: string
|
||||
--debug = false
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if not ($file_path | path exists) {
|
||||
error make {
|
||||
msg: $"File not found: ($file_path)"
|
||||
@ -391,7 +391,7 @@ export def rotate-encryption-keys [
|
||||
}
|
||||
|
||||
# Validate encryption configuration
|
||||
export def validate-encryption-config []: nothing -> record {
|
||||
export def validate-encryption-config [] {
|
||||
mut errors = []
|
||||
mut warnings = []
|
||||
|
||||
@ -472,7 +472,7 @@ export def validate-encryption-config []: nothing -> record {
|
||||
}
|
||||
|
||||
# Find SOPS configuration file
|
||||
def find-sops-config-path []: nothing -> string {
|
||||
def find-sops-config-path [] {
|
||||
# Check common locations
|
||||
let locations = [
|
||||
".sops.yaml"
|
||||
@ -494,7 +494,7 @@ def find-sops-config-path []: nothing -> string {
|
||||
# Check if config file contains sensitive data (heuristic)
|
||||
export def contains-sensitive-data [
|
||||
file_path: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
if not ($file_path | path exists) {
|
||||
return false
|
||||
}
|
||||
@ -520,7 +520,7 @@ export def contains-sensitive-data [
|
||||
export def scan-unencrypted-configs [
|
||||
directory: string
|
||||
--recursive = true
|
||||
]: nothing -> table {
|
||||
] {
|
||||
mut results = []
|
||||
|
||||
let files = if $recursive {
|
||||
@ -549,7 +549,7 @@ export def encrypt-sensitive-configs [
|
||||
--kms: string = "age"
|
||||
--dry-run = false
|
||||
--recursive = true
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"🔍 Scanning for unencrypted sensitive configs in ($directory)"
|
||||
|
||||
let unencrypted = (scan-unencrypted-configs $directory --recursive=$recursive)
|
||||
|
||||
@ -110,7 +110,7 @@ export def run-encryption-tests [
|
||||
}
|
||||
|
||||
# Test 1: Encryption detection
|
||||
def test-encryption-detection []: nothing -> record {
|
||||
def test-encryption-detection [] {
|
||||
let test_name = "Encryption Detection"
|
||||
|
||||
let result = (do {
|
||||
@ -148,7 +148,7 @@ def test-encryption-detection []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 2: Encrypt/Decrypt round-trip
|
||||
def test-encrypt-decrypt-roundtrip []: nothing -> record {
|
||||
def test-encrypt-decrypt-roundtrip [] {
|
||||
let test_name = "Encrypt/Decrypt Round-trip"
|
||||
|
||||
let result = (do {
|
||||
@ -228,7 +228,7 @@ def test-encrypt-decrypt-roundtrip []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 3: Memory-only decryption
|
||||
def test-memory-only-decryption []: nothing -> record {
|
||||
def test-memory-only-decryption [] {
|
||||
let test_name = "Memory-Only Decryption"
|
||||
|
||||
let result = (do {
|
||||
@ -301,7 +301,7 @@ def test-memory-only-decryption []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 4: Sensitive data detection
|
||||
def test-sensitive-data-detection []: nothing -> record {
|
||||
def test-sensitive-data-detection [] {
|
||||
let test_name = "Sensitive Data Detection"
|
||||
|
||||
let result = (do {
|
||||
@ -349,7 +349,7 @@ def test-sensitive-data-detection []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 5: KMS backend integration
|
||||
def test-kms-backend-integration []: nothing -> record {
|
||||
def test-kms-backend-integration [] {
|
||||
let test_name = "KMS Backend Integration"
|
||||
|
||||
let result = (do {
|
||||
@ -394,7 +394,7 @@ def test-kms-backend-integration []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 6: Config loader integration
|
||||
def test-config-loader-integration []: nothing -> record {
|
||||
def test-config-loader-integration [] {
|
||||
let test_name = "Config Loader Integration"
|
||||
|
||||
let result = (do {
|
||||
@ -438,7 +438,7 @@ def test-config-loader-integration []: nothing -> record {
|
||||
}
|
||||
|
||||
# Test 7: Encryption validation
|
||||
def test-encryption-validation []: nothing -> record {
|
||||
def test-encryption-validation [] {
|
||||
let test_name = "Encryption Validation"
|
||||
|
||||
let result = (do {
|
||||
|
||||
172
nulib/lib_provisioning/config/helpers/environment.nu
Normal file
172
nulib/lib_provisioning/config/helpers/environment.nu
Normal file
@ -0,0 +1,172 @@
|
||||
# Environment detection and management helper functions
|
||||
# NUSHELL 0.109 COMPLIANT - Using do-complete (Rule 5), each (Rule 8)
|
||||
|
||||
# Detect current environment from system context
|
||||
# Priority: PROVISIONING_ENV > CI/CD > git/dev markers > HOSTNAME > NODE_ENV > TERM > default
|
||||
export def detect-current-environment [] {
|
||||
# Check explicit environment variable
|
||||
if ($env.PROVISIONING_ENV? | is-not-empty) {
|
||||
return $env.PROVISIONING_ENV
|
||||
}
|
||||
|
||||
# Check CI/CD environments
|
||||
if ($env.CI? | is-not-empty) {
|
||||
if ($env.GITHUB_ACTIONS? | is-not-empty) { return "ci" }
|
||||
if ($env.GITLAB_CI? | is-not-empty) { return "ci" }
|
||||
if ($env.JENKINS_URL? | is-not-empty) { return "ci" }
|
||||
return "test"
|
||||
}
|
||||
|
||||
# Check for development indicators
|
||||
if (($env.PWD | path join ".git" | path exists) or
|
||||
($env.PWD | path join "development" | path exists) or
|
||||
($env.PWD | path join "dev" | path exists)) {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
# Check for production indicators
|
||||
if (($env.HOSTNAME? | default "" | str contains "prod") or
|
||||
($env.NODE_ENV? | default "" | str downcase) == "production" or
|
||||
($env.ENVIRONMENT? | default "" | str downcase) == "production") {
|
||||
return "prod"
|
||||
}
|
||||
|
||||
# Check for test indicators
|
||||
if (($env.NODE_ENV? | default "" | str downcase) == "test" or
|
||||
($env.ENVIRONMENT? | default "" | str downcase) == "test") {
|
||||
return "test"
|
||||
}
|
||||
|
||||
# Default to development for interactive usage
|
||||
if ($env.TERM? | is-not-empty) {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
# Fallback
|
||||
"dev"
|
||||
}
|
||||
|
||||
# Get available environments from configuration
|
||||
export def get-available-environments [config: record] {
|
||||
let env_section_result = (do { $config | get "environments" } | complete)
|
||||
let environments_section = if $env_section_result.exit_code == 0 { $env_section_result.stdout } else { {} }
|
||||
$environments_section | columns
|
||||
}
|
||||
|
||||
# Validate environment name
|
||||
export def validate-environment [environment: string, config: record] {
|
||||
let valid_environments = ["dev" "test" "prod" "ci" "staging" "local"]
|
||||
let configured_environments = (get-available-environments $config)
|
||||
let all_valid = ($valid_environments | append $configured_environments | uniq)
|
||||
|
||||
if ($environment in $all_valid) {
|
||||
{ valid: true, message: "" }
|
||||
} else {
|
||||
{
|
||||
valid: false,
|
||||
message: $"Invalid environment '($environment)'. Valid options: ($all_valid | str join ', ')"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set a configuration value using dot notation path (e.g., "debug.log_level")
|
||||
def set-config-value [config: record, path: string, value: any] {
|
||||
let path_parts = ($path | split row ".")
|
||||
|
||||
match ($path_parts | length) {
|
||||
1 => {
|
||||
$config | upsert ($path_parts | first) $value
|
||||
}
|
||||
2 => {
|
||||
let section = ($path_parts | first)
|
||||
let key = ($path_parts | last)
|
||||
let section_result = (do { $config | get $section } | complete)
|
||||
let section_data = if $section_result.exit_code == 0 { $section_result.stdout } else { {} }
|
||||
$config | upsert $section ($section_data | upsert $key $value)
|
||||
}
|
||||
3 => {
|
||||
let section = ($path_parts | first)
|
||||
let subsection = ($path_parts | get 1)
|
||||
let key = ($path_parts | last)
|
||||
let section_result = (do { $config | get $section } | complete)
|
||||
let section_data = if $section_result.exit_code == 0 { $section_result.stdout } else { {} }
|
||||
let subsection_result = (do { $section_data | get $subsection } | complete)
|
||||
let subsection_data = if $subsection_result.exit_code == 0 { $subsection_result.stdout } else { {} }
|
||||
$config | upsert $section ($section_data | upsert $subsection ($subsection_data | upsert $key $value))
|
||||
}
|
||||
_ => {
|
||||
# For deeper nesting, use recursive approach
|
||||
set-config-value-recursive $config $path_parts $value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Recursive helper for deep config value setting
|
||||
def set-config-value-recursive [config: record, path_parts: list, value: any] {
|
||||
if ($path_parts | length) == 1 {
|
||||
$config | upsert ($path_parts | first) $value
|
||||
} else {
|
||||
let current_key = ($path_parts | first)
|
||||
let remaining_parts = ($path_parts | skip 1)
|
||||
let current_result = (do { $config | get $current_key } | complete)
|
||||
let current_section = if $current_result.exit_code == 0 { $current_result.stdout } else { {} }
|
||||
$config | upsert $current_key (set-config-value-recursive $current_section $remaining_parts $value)
|
||||
}
|
||||
}
|
||||
|
||||
# Apply environment variable overrides to configuration
|
||||
export def apply-environment-variable-overrides [config: record, debug = false] {
|
||||
# Map of environment variables to config paths with type conversion
|
||||
let env_mappings = {
|
||||
"PROVISIONING_DEBUG": { path: "debug.enabled", type: "bool" },
|
||||
"PROVISIONING_LOG_LEVEL": { path: "debug.log_level", type: "string" },
|
||||
"PROVISIONING_NO_TERMINAL": { path: "debug.no_terminal", type: "bool" },
|
||||
"PROVISIONING_CHECK": { path: "debug.check", type: "bool" },
|
||||
"PROVISIONING_METADATA": { path: "debug.metadata", type: "bool" },
|
||||
"PROVISIONING_OUTPUT_FORMAT": { path: "output.format", type: "string" },
|
||||
"PROVISIONING_FILE_VIEWER": { path: "output.file_viewer", type: "string" },
|
||||
"PROVISIONING_USE_SOPS": { path: "sops.use_sops", type: "bool" },
|
||||
"PROVISIONING_PROVIDER": { path: "providers.default", type: "string" },
|
||||
"PROVISIONING_WORKSPACE_PATH": { path: "paths.workspace", type: "string" },
|
||||
"PROVISIONING_INFRA_PATH": { path: "paths.infra", type: "string" },
|
||||
"PROVISIONING_SOPS": { path: "sops.config_path", type: "string" },
|
||||
"PROVISIONING_KAGE": { path: "sops.age_key_file", type: "string" }
|
||||
}
|
||||
|
||||
# Use reduce --fold to process all env mappings (Rule 3: no mutable variables)
|
||||
$env_mappings | columns | reduce --fold $config {|env_var, result|
|
||||
let env_result = (do { $env | get $env_var } | complete)
|
||||
let env_value = if $env_result.exit_code == 0 { $env_result.stdout } else { null }
|
||||
|
||||
if ($env_value | is-not-empty) {
|
||||
let mapping = ($env_mappings | get $env_var)
|
||||
let config_path = $mapping.path
|
||||
let config_type = $mapping.type
|
||||
|
||||
# Convert value to appropriate type
|
||||
let converted_value = match $config_type {
|
||||
"bool" => {
|
||||
if ($env_value | describe) == "string" {
|
||||
match ($env_value | str downcase) {
|
||||
"true" | "1" | "yes" | "on" => true
|
||||
"false" | "0" | "no" | "off" => false
|
||||
_ => false
|
||||
}
|
||||
} else {
|
||||
$env_value | into bool
|
||||
}
|
||||
}
|
||||
"string" => $env_value
|
||||
_ => $env_value
|
||||
}
|
||||
|
||||
if $debug {
|
||||
# log debug $"Applying env override: ($env_var) -> ($config_path) = ($converted_value)"
|
||||
}
|
||||
|
||||
(set-config-value $result $config_path $converted_value)
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
}
|
||||
}
|
||||
26
nulib/lib_provisioning/config/helpers/merging.nu
Normal file
26
nulib/lib_provisioning/config/helpers/merging.nu
Normal file
@ -0,0 +1,26 @@
|
||||
# Configuration merging helper functions
|
||||
# NUSHELL 0.109 COMPLIANT - Using reduce --fold (Rule 3), no mutable variables
|
||||
|
||||
# Deep merge two configuration records (right takes precedence)
|
||||
# Uses reduce --fold instead of mutable variables (Nushell 0.109 Rule 3)
|
||||
export def deep-merge [
|
||||
base: record
|
||||
override: record
|
||||
]: record -> record {
|
||||
$override | columns | reduce --fold $base {|key, result|
|
||||
let override_value = ($override | get $key)
|
||||
let base_result = (do { $base | get $key } | complete)
|
||||
let base_value = if $base_result.exit_code == 0 { $base_result.stdout } else { null }
|
||||
|
||||
if ($base_value | is-empty) {
|
||||
# Key doesn't exist in base, add it
|
||||
($result | insert $key $override_value)
|
||||
} else if (($base_value | describe) | str starts-with "record") and (($override_value | describe) | str starts-with "record") {
|
||||
# Both are records, merge recursively (Nushell Rule 1: type detection via describe)
|
||||
($result | upsert $key (deep-merge $base_value $override_value))
|
||||
} else {
|
||||
# Override the value
|
||||
($result | upsert $key $override_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
88
nulib/lib_provisioning/config/helpers/workspace.nu
Normal file
88
nulib/lib_provisioning/config/helpers/workspace.nu
Normal file
@ -0,0 +1,88 @@
|
||||
# Workspace management helper functions
|
||||
# NUSHELL 0.109 COMPLIANT - Using each (Rule 8), no mutable variables (Rule 3)
|
||||
|
||||
# Get the currently active workspace
|
||||
export def get-active-workspace [] {
|
||||
let user_config_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
||||
|
||||
if not ($user_config_dir | path exists) {
|
||||
return null
|
||||
}
|
||||
|
||||
# Load central user config
|
||||
let user_config_path = ($user_config_dir | path join "user_config.yaml")
|
||||
|
||||
if not ($user_config_path | path exists) {
|
||||
return null
|
||||
}
|
||||
|
||||
let user_config = (open $user_config_path)
|
||||
|
||||
# Check if active workspace is set
|
||||
if ($user_config.active_workspace == null) {
|
||||
null
|
||||
} else {
|
||||
# Find workspace in list
|
||||
let workspace_name = $user_config.active_workspace
|
||||
let workspace = ($user_config.workspaces | where name == $workspace_name | first)
|
||||
|
||||
if ($workspace | is-empty) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
name: $workspace.name
|
||||
path: $workspace.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Update workspace last used timestamp (internal)
|
||||
export def update-workspace-last-used [workspace_name: string] {
|
||||
let user_config_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
||||
let user_config_path = ($user_config_dir | path join "user_config.yaml")
|
||||
|
||||
if not ($user_config_path | path exists) {
|
||||
return
|
||||
}
|
||||
|
||||
let user_config = (open $user_config_path)
|
||||
|
||||
# Update last_used timestamp for workspace
|
||||
let updated_config = (
|
||||
$user_config | upsert workspaces {|ws|
|
||||
$ws | each {|w|
|
||||
if $w.name == $workspace_name {
|
||||
$w | upsert last_used (date now | format date '%Y-%m-%dT%H:%M:%SZ')
|
||||
} else {
|
||||
$w
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$updated_config | to yaml | save --force $user_config_path
|
||||
}
|
||||
|
||||
# Get project root directory
|
||||
export def get-project-root [] {
|
||||
let markers = [".provisioning.toml", "provisioning.toml", ".git", "provisioning"]
|
||||
|
||||
let mut current = ($env.PWD | path expand)
|
||||
|
||||
while $current != "/" {
|
||||
let found = ($markers
|
||||
| any {|marker|
|
||||
(($current | path join $marker) | path exists)
|
||||
}
|
||||
)
|
||||
|
||||
if $found {
|
||||
return $current
|
||||
}
|
||||
|
||||
$current = ($current | path dirname)
|
||||
}
|
||||
|
||||
$env.PWD
|
||||
}
|
||||
343
nulib/lib_provisioning/config/interpolation/core.nu
Normal file
343
nulib/lib_provisioning/config/interpolation/core.nu
Normal file
@ -0,0 +1,343 @@
|
||||
# Configuration interpolation - Substitutes variables and patterns in config
|
||||
# NUSHELL 0.109 COMPLIANT - Using reduce --fold (Rule 3), do-complete (Rule 5), each (Rule 8)
|
||||
|
||||
use ../helpers/environment.nu *
|
||||
|
||||
# Main interpolation entry point - interpolates all patterns in configuration
|
||||
export def interpolate-config [config: record]: nothing -> record {
|
||||
let base_result = (do { $config | get paths.base } | complete)
|
||||
let base_path = if $base_result.exit_code == 0 { $base_result.stdout } else { "" }
|
||||
|
||||
if ($base_path | is-not-empty) {
|
||||
# Convert config to JSON, apply all interpolations, convert back
|
||||
let json_str = ($config | to json)
|
||||
let interpolated_json = (interpolate-all-patterns $json_str $config)
|
||||
($interpolated_json | from json)
|
||||
} else {
|
||||
$config
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate a single string value with configuration context
|
||||
export def interpolate-string [text: string, config: record]: nothing -> string {
|
||||
# Basic interpolation for {{paths.base}} pattern
|
||||
if ($text | str contains "{{paths.base}}") {
|
||||
let base_path = (get-config-value $config "paths.base" "")
|
||||
($text | str replace --all "{{paths.base}}" $base_path)
|
||||
} else {
|
||||
$text
|
||||
}
|
||||
}
|
||||
|
||||
# Get a nested configuration value using dot notation
|
||||
export def get-config-value [config: record, path: string, default_value: any]: nothing -> any {
|
||||
let path_parts = ($path | split row ".")
|
||||
|
||||
# Navigate to the value using the path
|
||||
let result = ($path_parts | reduce --fold $config {|part, current|
|
||||
let access_result = (do { $current | get $part } | complete)
|
||||
if $access_result.exit_code == 0 { $access_result.stdout } else { null }
|
||||
})
|
||||
|
||||
if ($result | is-empty) { $default_value } else { $result }
|
||||
}
|
||||
|
||||
# Apply all interpolation patterns to JSON string (Rule 3: using reduce --fold for sequence)
|
||||
def interpolate-all-patterns [json_str: string, config: record]: nothing -> string {
|
||||
# Apply each interpolation pattern in sequence using reduce --fold
|
||||
# This ensures patterns are applied in order and mutations are immutable
|
||||
let patterns = [
|
||||
{name: "paths.base", fn: {|s, c| interpolate-base-path $s ($c | get paths.base | default "") }}
|
||||
{name: "env", fn: {|s, c| interpolate-env-variables $s}}
|
||||
{name: "datetime", fn: {|s, c| interpolate-datetime $s}}
|
||||
{name: "git", fn: {|s, c| interpolate-git-info $s}}
|
||||
{name: "sops", fn: {|s, c| interpolate-sops-config $s $c}}
|
||||
{name: "providers", fn: {|s, c| interpolate-provider-refs $s $c}}
|
||||
{name: "advanced", fn: {|s, c| interpolate-advanced-features $s $c}}
|
||||
]
|
||||
|
||||
$patterns | reduce --fold $json_str {|pattern, result|
|
||||
do { ($pattern.fn | call $result $config) } | complete | if $in.exit_code == 0 { $in.stdout } else { $result }
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate base path pattern
|
||||
def interpolate-base-path [text: string, base_path: string]: nothing -> string {
|
||||
if ($text | str contains "{{paths.base}}") {
|
||||
($text | str replace --all "{{paths.base}}" $base_path)
|
||||
} else {
|
||||
$text
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate environment variables with security validation (Rule 8: using reduce --fold)
|
||||
def interpolate-env-variables [text: string]: nothing -> string {
|
||||
# Safe environment variables list (security allowlist)
|
||||
let safe_env_vars = [
|
||||
"HOME" "USER" "HOSTNAME" "PWD" "SHELL"
|
||||
"PROVISIONING" "PROVISIONING_WORKSPACE_PATH" "PROVISIONING_INFRA_PATH"
|
||||
"PROVISIONING_SOPS" "PROVISIONING_KAGE"
|
||||
]
|
||||
|
||||
# Apply each env var substitution using reduce --fold (Rule 3: no mutable variables)
|
||||
let with_env = ($safe_env_vars | reduce --fold $text {|env_var, result|
|
||||
let pattern = $"\\{\\{env\\.($env_var)\\}\\}"
|
||||
let env_result = (do { $env | get $env_var } | complete)
|
||||
let env_value = if $env_result.exit_code == 0 { $env_result.stdout } else { "" }
|
||||
|
||||
if ($env_value | is-not-empty) {
|
||||
($result | str replace --regex $pattern $env_value)
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
})
|
||||
|
||||
# Handle conditional environment variables
|
||||
interpolate-conditional-env $with_env
|
||||
}
|
||||
|
||||
# Handle conditional environment variable interpolation
|
||||
def interpolate-conditional-env [text: string]: nothing -> string {
|
||||
let conditionals = [
|
||||
{pattern: "{{env.HOME || \"/tmp\"}}", value: {|| ($env.HOME? | default "/tmp")}}
|
||||
{pattern: "{{env.USER || \"unknown\"}}", value: {|| ($env.USER? | default "unknown")}}
|
||||
]
|
||||
|
||||
$conditionals | reduce --fold $text {|cond, result|
|
||||
if ($result | str contains $cond.pattern) {
|
||||
let value = (($cond.value | call))
|
||||
($result | str replace --all $cond.pattern $value)
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate date and time values
|
||||
def interpolate-datetime [text: string]: nothing -> string {
|
||||
let current_date = (date now | format date "%Y-%m-%d")
|
||||
let current_timestamp = (date now | format date "%s")
|
||||
let iso_timestamp = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
let with_date = ($text | str replace --all "{{now.date}}" $current_date)
|
||||
let with_timestamp = ($with_date | str replace --all "{{now.timestamp}}" $current_timestamp)
|
||||
($with_timestamp | str replace --all "{{now.iso}}" $iso_timestamp)
|
||||
}
|
||||
|
||||
# Interpolate git information (defaults to "unknown" to avoid hanging)
|
||||
def interpolate-git-info [text: string]: nothing -> string {
|
||||
let patterns = [
|
||||
{pattern: "{{git.branch}}", value: "unknown"}
|
||||
{pattern: "{{git.commit}}", value: "unknown"}
|
||||
{pattern: "{{git.origin}}", value: "unknown"}
|
||||
]
|
||||
|
||||
$patterns | reduce --fold $text {|p, result|
|
||||
($result | str replace --all $p.pattern $p.value)
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate SOPS configuration references
|
||||
def interpolate-sops-config [text: string, config: record]: nothing -> string {
|
||||
let sops_key_result = (do { $config | get sops.age_key_file } | complete)
|
||||
let sops_key_file = if $sops_key_result.exit_code == 0 { $sops_key_result.stdout } else { "" }
|
||||
|
||||
let with_key = if ($sops_key_file | is-not-empty) {
|
||||
($text | str replace --all "{{sops.key_file}}" $sops_key_file)
|
||||
} else {
|
||||
$text
|
||||
}
|
||||
|
||||
let sops_cfg_result = (do { $config | get sops.config_path } | complete)
|
||||
let sops_config_path = if $sops_cfg_result.exit_code == 0 { $sops_cfg_result.stdout } else { "" }
|
||||
|
||||
if ($sops_config_path | is-not-empty) {
|
||||
($with_key | str replace --all "{{sops.config_path}}" $sops_config_path)
|
||||
} else {
|
||||
$with_key
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate cross-section provider references
|
||||
def interpolate-provider-refs [text: string, config: record]: nothing -> string {
|
||||
let providers_to_check = [
|
||||
{pattern: "{{providers.aws.region}}", path: "providers.aws.region"}
|
||||
{pattern: "{{providers.default}}", path: "providers.default"}
|
||||
{pattern: "{{providers.upcloud.zone}}", path: "providers.upcloud.zone"}
|
||||
]
|
||||
|
||||
$providers_to_check | reduce --fold $text {|prov, result|
|
||||
let value_result = (do {
|
||||
let parts = ($prov.path | split row ".")
|
||||
if ($parts | length) == 2 {
|
||||
$config | get ($parts | first) | get ($parts | last)
|
||||
} else {
|
||||
$config | get ($parts | first) | get ($parts | get 1) | get ($parts | last)
|
||||
}
|
||||
} | complete)
|
||||
|
||||
let value = if $value_result.exit_code == 0 { $value_result.stdout } else { "" }
|
||||
|
||||
if ($value | is-not-empty) {
|
||||
($result | str replace --all $prov.pattern $value)
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Interpolate advanced features (function calls, environment-aware paths)
|
||||
def interpolate-advanced-features [text: string, config: record]: nothing -> string {
|
||||
let base_path_result = (do { $config | get paths.base } | complete)
|
||||
let base_path = if $base_path_result.exit_code == 0 { $base_path_result.stdout } else { "" }
|
||||
|
||||
let with_path_join = if ($text | str contains "{{path.join(paths.base") {
|
||||
# Simple regex-based path.join replacement
|
||||
($text | str replace --regex "\\{\\{path\\.join\\(paths\\.base,\\s*\"([^\"]+)\"\\)\\}\\}" $"($base_path)/$1")
|
||||
} else {
|
||||
$text
|
||||
}
|
||||
|
||||
# Replace environment-aware paths
|
||||
let current_env_result = (do { $config | get current_environment } | complete)
|
||||
let current_env = if $current_env_result.exit_code == 0 { $current_env_result.stdout } else { "dev" }
|
||||
|
||||
($with_path_join | str replace --all "{{paths.base.\${env}}}" $"{{paths.base}}.($current_env)")
|
||||
}
|
||||
|
||||
# Validate interpolation patterns and detect issues
|
||||
export def validate-interpolation [
|
||||
config: record
|
||||
--detailed = false
|
||||
]: nothing -> record {
|
||||
let json_str = ($config | to json)
|
||||
|
||||
# Check for unresolved interpolation patterns
|
||||
let unresolved = (detect-unresolved-patterns $json_str)
|
||||
let unresolved_errors = if ($unresolved | length) > 0 {
|
||||
[{
|
||||
type: "unresolved_interpolation",
|
||||
severity: "error",
|
||||
patterns: $unresolved,
|
||||
message: $"Unresolved interpolation patterns found: ($unresolved | str join ', ')"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Check for circular dependencies
|
||||
let circular = (detect-circular-dependencies $json_str)
|
||||
let circular_errors = if ($circular | length) > 0 {
|
||||
[{
|
||||
type: "circular_dependency",
|
||||
severity: "error",
|
||||
dependencies: $circular,
|
||||
message: $"Circular interpolation dependencies detected"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Check for unsafe environment variable access
|
||||
let unsafe = (detect-unsafe-env-patterns $json_str)
|
||||
let unsafe_warnings = if ($unsafe | length) > 0 {
|
||||
[{
|
||||
type: "unsafe_env_access",
|
||||
severity: "warning",
|
||||
variables: $unsafe,
|
||||
message: $"Potentially unsafe environment variable access"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Validate git context if needed
|
||||
let git_warnings = if ($json_str | str contains "{{git.") {
|
||||
let git_check = (do { ^git rev-parse --git-dir err> /dev/null } | complete)
|
||||
if ($git_check.exit_code != 0) {
|
||||
[{
|
||||
type: "git_context",
|
||||
severity: "warning",
|
||||
message: "Git interpolation patterns found but not in a git repository"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Combine all results
|
||||
let all_errors = ($unresolved_errors | append $circular_errors)
|
||||
let all_warnings = ($unsafe_warnings | append $git_warnings)
|
||||
|
||||
if (not $detailed) and (($all_errors | length) > 0) {
|
||||
let error_messages = ($all_errors | each { |err| $err.message })
|
||||
error make {msg: ($error_messages | str join "; ")}
|
||||
}
|
||||
|
||||
{
|
||||
valid: (($all_errors | length) == 0),
|
||||
errors: $all_errors,
|
||||
warnings: $all_warnings,
|
||||
summary: {
|
||||
total_errors: ($all_errors | length),
|
||||
total_warnings: ($all_warnings | length),
|
||||
interpolation_patterns_detected: (count-interpolation-patterns $json_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Detect unresolved interpolation patterns
|
||||
def detect-unresolved-patterns [text: string]: nothing -> list {
|
||||
# Known patterns that should be handled
|
||||
let known_prefixes = ["paths" "env" "now" "git" "sops" "providers" "path"]
|
||||
|
||||
# Extract all {{...}} patterns and check if they match known types
|
||||
let all_patterns = (do {
|
||||
$text | str replace --regex "\\{\\{([^}]+)\\}\\}" "$1"
|
||||
} | complete)
|
||||
|
||||
if ($all_patterns.exit_code != 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
# Check for unknown patterns (simplified detection)
|
||||
if ($text | str contains "{{unknown.") {
|
||||
["unknown.*"]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Detect circular interpolation dependencies
|
||||
def detect-circular-dependencies [text: string]: nothing -> list {
|
||||
if (($text | str contains "{{paths.base}}") and ($text | str contains "paths.base.*{{paths.base}}")) {
|
||||
["paths.base -> paths.base"]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Detect unsafe environment variable patterns
|
||||
def detect-unsafe-env-patterns [text: string]: nothing -> list {
|
||||
let dangerous_patterns = ["PATH" "LD_LIBRARY_PATH" "PYTHONPATH" "SHELL" "PS1"]
|
||||
|
||||
# Use reduce --fold to find all unsafe patterns (Rule 3)
|
||||
$dangerous_patterns | reduce --fold [] {|pattern, unsafe_list|
|
||||
if ($text | str contains $"{{env.($pattern)}}") {
|
||||
($unsafe_list | append $pattern)
|
||||
} else {
|
||||
$unsafe_list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Count interpolation patterns in text for metrics
|
||||
def count-interpolation-patterns [text: string]: nothing -> number {
|
||||
# Count {{...}} occurrences
|
||||
($text | str replace --all --regex "\\{\\{[^}]+\\}\\}" "" | length) - ($text | length)
|
||||
| math abs
|
||||
| ($text | length) - .
|
||||
| . / 4 # Approximate based on {{ }} length
|
||||
}
|
||||
@ -69,7 +69,7 @@ def get-minimal-config [
|
||||
}
|
||||
|
||||
# Check if a command needs full config loading
|
||||
export def command-needs-full-config [command: string]: nothing -> bool {
|
||||
export def command-needs-full-config [command: string] {
|
||||
let fast_commands = [
|
||||
"help", "version", "status", "workspace list", "workspace active",
|
||||
"plugin list", "env", "nu"
|
||||
|
||||
@ -97,7 +97,7 @@ export def get-defaults-config-path [] {
|
||||
}
|
||||
|
||||
# Check if a file is encrypted with SOPS
|
||||
export def check-if-sops-encrypted [file_path: string]: nothing -> bool {
|
||||
export def check-if-sops-encrypted [file_path: string] {
|
||||
let file_exists = ($file_path | path exists)
|
||||
if not $file_exists {
|
||||
return false
|
||||
|
||||
@ -141,10 +141,15 @@ export def load-provisioning-config [
|
||||
|
||||
# If Nickel config exists, ensure it's exported
|
||||
if ($workspace_config_ncl | path exists) {
|
||||
try {
|
||||
let export_result = (do {
|
||||
use ../config/export.nu *
|
||||
export-all-configs $active_workspace.path
|
||||
} catch { }
|
||||
} | complete)
|
||||
if $export_result.exit_code != 0 {
|
||||
if $debug {
|
||||
# log debug $"Nickel export failed: ($export_result.stderr)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Load from generated directory (preferred)
|
||||
@ -191,10 +196,11 @@ export def load-provisioning-config [
|
||||
|
||||
let workspace_config = if ($ncl_config | path exists) {
|
||||
# Export Nickel config to TOML
|
||||
try {
|
||||
let export_result = (do {
|
||||
use ../config/export.nu *
|
||||
export-all-configs $env.PWD
|
||||
} catch {
|
||||
} | complete)
|
||||
if $export_result.exit_code != 0 {
|
||||
# Silently continue if export fails
|
||||
}
|
||||
{
|
||||
@ -244,9 +250,12 @@ export def load-provisioning-config [
|
||||
$config_data
|
||||
} else if ($config_data | type | str contains "string") {
|
||||
# If we got a string, try to parse it as YAML
|
||||
try {
|
||||
let yaml_result = (do {
|
||||
$config_data | from yaml
|
||||
} catch {
|
||||
} | complete)
|
||||
if $yaml_result.exit_code == 0 {
|
||||
$yaml_result.stdout
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
} else {
|
||||
@ -274,7 +283,9 @@ export def load-provisioning-config [
|
||||
|
||||
# Apply environment-specific overrides from environments section
|
||||
if ($current_environment | is-not-empty) {
|
||||
let env_config = ($final_config | try { get $"environments.($current_environment)" } catch { {} })
|
||||
let current_config = $final_config
|
||||
let env_result = (do { $current_config | get $"environments.($current_environment)" } | complete)
|
||||
let env_config = if $env_result.exit_code == 0 { $env_result.stdout } else { {} }
|
||||
if ($env_config | is-not-empty) {
|
||||
if $debug {
|
||||
# log debug $"Applying environment overrides for: ($current_environment)"
|
||||
@ -356,15 +367,19 @@ export def load-config-file [
|
||||
if $debug {
|
||||
# log debug $"Loading Nickel config file: ($file_path)"
|
||||
}
|
||||
try {
|
||||
return (nickel export --format json $file_path | from json)
|
||||
} catch {|e|
|
||||
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): ($e)"
|
||||
print $"❌ Failed to load Nickel config ($file_path): ($nickel_result.stderr)"
|
||||
exit 1
|
||||
} else {
|
||||
if $debug {
|
||||
# log debug $"Failed to load optional Nickel config: ($e)"
|
||||
# log debug $"Failed to load optional Nickel config: ($nickel_result.stderr)"
|
||||
}
|
||||
return {}
|
||||
}
|
||||
@ -532,7 +547,8 @@ export def deep-merge [
|
||||
|
||||
for key in ($override | columns) {
|
||||
let override_value = ($override | get $key)
|
||||
let base_value = ($base | try { get $key } catch { null })
|
||||
let base_result = (do { $base | get $key } | complete)
|
||||
let base_value = if $base_result.exit_code == 0 { $base_result.stdout } else { null }
|
||||
|
||||
if ($base_value | is-empty) {
|
||||
# Key doesn't exist in base, add it
|
||||
@ -556,7 +572,8 @@ export def interpolate-config [
|
||||
mut result = $config
|
||||
|
||||
# Get base path for interpolation
|
||||
let base_path = ($config | try { get paths.base } catch { ""})
|
||||
let base_result = (do { $config | get paths.base } | complete)
|
||||
let base_path = if $base_result.exit_code == 0 { $base_result.stdout } else { "" }
|
||||
|
||||
if ($base_path | is-not-empty) {
|
||||
# Interpolate the entire config structure
|
||||
@ -594,7 +611,9 @@ export def get-config-value [
|
||||
mut current = $config
|
||||
|
||||
for part in $path_parts {
|
||||
let next_value = ($current | try { get $part } catch { null })
|
||||
let immutable_current = $current
|
||||
let next_result = (do { $immutable_current | get $part } | complete)
|
||||
let next_value = if $next_result.exit_code == 0 { $next_result.stdout } else { null }
|
||||
if ($next_value | is-empty) {
|
||||
return $default_value
|
||||
}
|
||||
@ -613,7 +632,9 @@ export def validate-config-structure [
|
||||
mut warnings = []
|
||||
|
||||
for section in $required_sections {
|
||||
if ($config | try { get $section } catch { null } | is-empty) {
|
||||
let section_result = (do { $config | get $section } | complete)
|
||||
let section_value = if $section_result.exit_code == 0 { $section_result.stdout } else { null }
|
||||
if ($section_value | is-empty) {
|
||||
$errors = ($errors | append {
|
||||
type: "missing_section",
|
||||
severity: "error",
|
||||
@ -638,10 +659,12 @@ export def validate-path-values [
|
||||
mut errors = []
|
||||
mut warnings = []
|
||||
|
||||
let paths = ($config | try { get paths } catch { {} })
|
||||
let paths_result = (do { $config | get paths } | complete)
|
||||
let paths = if $paths_result.exit_code == 0 { $paths_result.stdout } else { {} }
|
||||
|
||||
for path_name in $required_paths {
|
||||
let path_value = ($paths | try { get $path_name } catch { null })
|
||||
let path_result = (do { $paths | get $path_name } | complete)
|
||||
let path_value = if $path_result.exit_code == 0 { $path_result.stdout } else { null }
|
||||
|
||||
if ($path_value | is-empty) {
|
||||
$errors = ($errors | append {
|
||||
@ -692,7 +715,8 @@ export def validate-data-types [
|
||||
mut warnings = []
|
||||
|
||||
# Validate core.version follows semantic versioning pattern
|
||||
let core_version = ($config | try { get core.version } catch { null })
|
||||
let core_result = (do { $config | get core.version } | complete)
|
||||
let core_version = if $core_result.exit_code == 0 { $core_result.stdout } else { null }
|
||||
if ($core_version | is-not-empty) {
|
||||
let version_pattern = "^\\d+\\.\\d+\\.\\d+(-.+)?$"
|
||||
let version_parts = ($core_version | split row ".")
|
||||
@ -708,7 +732,8 @@ export def validate-data-types [
|
||||
}
|
||||
|
||||
# Validate debug.enabled is boolean
|
||||
let debug_enabled = ($config | try { get debug.enabled } catch { null })
|
||||
let debug_result = (do { $config | get debug.enabled } | complete)
|
||||
let debug_enabled = if $debug_result.exit_code == 0 { $debug_result.stdout } else { null }
|
||||
if ($debug_enabled | is-not-empty) {
|
||||
if (($debug_enabled | describe) != "bool") {
|
||||
$errors = ($errors | append {
|
||||
@ -724,7 +749,8 @@ export def validate-data-types [
|
||||
}
|
||||
|
||||
# Validate debug.metadata is boolean
|
||||
let debug_metadata = ($config | try { get debug.metadata } catch { null })
|
||||
let debug_meta_result = (do { $config | get debug.metadata } | complete)
|
||||
let debug_metadata = if $debug_meta_result.exit_code == 0 { $debug_meta_result.stdout } else { null }
|
||||
if ($debug_metadata | is-not-empty) {
|
||||
if (($debug_metadata | describe) != "bool") {
|
||||
$errors = ($errors | append {
|
||||
@ -740,7 +766,8 @@ export def validate-data-types [
|
||||
}
|
||||
|
||||
# Validate sops.use_sops is boolean
|
||||
let sops_use = ($config | try { get sops.use_sops } catch { null })
|
||||
let sops_result = (do { $config | get sops.use_sops } | complete)
|
||||
let sops_use = if $sops_result.exit_code == 0 { $sops_result.stdout } else { null }
|
||||
if ($sops_use | is-not-empty) {
|
||||
if (($sops_use | describe) != "bool") {
|
||||
$errors = ($errors | append {
|
||||
@ -770,8 +797,10 @@ export def validate-semantic-rules [
|
||||
mut warnings = []
|
||||
|
||||
# Validate provider configuration
|
||||
let providers = ($config | try { get providers } catch { {} })
|
||||
let default_provider = ($providers | try { get default } catch { null })
|
||||
let providers_result = (do { $config | get providers } | complete)
|
||||
let providers = if $providers_result.exit_code == 0 { $providers_result.stdout } else { {} }
|
||||
let default_result = (do { $providers | get default } | complete)
|
||||
let default_provider = if $default_result.exit_code == 0 { $default_result.stdout } else { null }
|
||||
|
||||
if ($default_provider | is-not-empty) {
|
||||
let valid_providers = ["aws", "upcloud", "local"]
|
||||
@ -788,7 +817,8 @@ export def validate-semantic-rules [
|
||||
}
|
||||
|
||||
# Validate log level
|
||||
let log_level = ($config | try { get debug.log_level } catch { null })
|
||||
let log_level_result = (do { $config | get debug.log_level } | complete)
|
||||
let log_level = if $log_level_result.exit_code == 0 { $log_level_result.stdout } else { null }
|
||||
if ($log_level | is-not-empty) {
|
||||
let valid_levels = ["trace", "debug", "info", "warn", "error"]
|
||||
if not ($log_level in $valid_levels) {
|
||||
@ -804,7 +834,8 @@ export def validate-semantic-rules [
|
||||
}
|
||||
|
||||
# Validate output format
|
||||
let output_format = ($config | try { get output.format } catch { null })
|
||||
let output_result = (do { $config | get output.format } | complete)
|
||||
let output_format = if $output_result.exit_code == 0 { $output_result.stdout } else { null }
|
||||
if ($output_format | is-not-empty) {
|
||||
let valid_formats = ["json", "yaml", "toml", "text"]
|
||||
if not ($output_format in $valid_formats) {
|
||||
@ -834,7 +865,8 @@ export def validate-file-existence [
|
||||
mut warnings = []
|
||||
|
||||
# Check SOPS configuration file
|
||||
let sops_config = ($config | try { get sops.config_path } catch { null })
|
||||
let sops_cfg_result = (do { $config | get sops.config_path } | complete)
|
||||
let sops_config = if $sops_cfg_result.exit_code == 0 { $sops_cfg_result.stdout } else { null }
|
||||
if ($sops_config | is-not-empty) {
|
||||
if not ($sops_config | path exists) {
|
||||
$warnings = ($warnings | append {
|
||||
@ -848,7 +880,8 @@ export def validate-file-existence [
|
||||
}
|
||||
|
||||
# Check SOPS key files
|
||||
let key_paths = ($config | try { get sops.key_search_paths } catch { [] })
|
||||
let key_result = (do { $config | get sops.key_search_paths } | complete)
|
||||
let key_paths = if $key_result.exit_code == 0 { $key_result.stdout } else { [] }
|
||||
mut found_key = false
|
||||
|
||||
for key_path in $key_paths {
|
||||
@ -870,7 +903,8 @@ export def validate-file-existence [
|
||||
}
|
||||
|
||||
# Check critical configuration files
|
||||
let settings_file = ($config | try { get paths.files.settings } catch { null })
|
||||
let settings_result = (do { $config | get paths.files.settings } | complete)
|
||||
let settings_file = if $settings_result.exit_code == 0 { $settings_result.stdout } else { null }
|
||||
if ($settings_file | is-not-empty) {
|
||||
if not ($settings_file | path exists) {
|
||||
$errors = ($errors | append {
|
||||
@ -1126,7 +1160,8 @@ def interpolate-env-variables [
|
||||
|
||||
for env_var in $safe_env_vars {
|
||||
let pattern = $"\\{\\{env\\.($env_var)\\}\\}"
|
||||
let env_value = ($env | try { get $env_var } catch { ""})
|
||||
let env_result = (do { $env | get $env_var } | complete)
|
||||
let env_value = if $env_result.exit_code == 0 { $env_result.stdout } else { "" }
|
||||
if ($env_value | is-not-empty) {
|
||||
$result = ($result | str replace --regex $pattern $env_value)
|
||||
}
|
||||
@ -1209,13 +1244,15 @@ def interpolate-sops-config [
|
||||
mut result = $text
|
||||
|
||||
# SOPS key file path
|
||||
let sops_key_file = ($config | try { get sops.age_key_file } catch { ""})
|
||||
let sops_key_result = (do { $config | get sops.age_key_file } | complete)
|
||||
let sops_key_file = if $sops_key_result.exit_code == 0 { $sops_key_result.stdout } else { "" }
|
||||
if ($sops_key_file | is-not-empty) {
|
||||
$result = ($result | str replace --all "{{sops.key_file}}" $sops_key_file)
|
||||
}
|
||||
|
||||
# SOPS config path
|
||||
let sops_config_path = ($config | try { get sops.config_path } catch { ""})
|
||||
let sops_cfg_path_result = (do { $config | get sops.config_path } | complete)
|
||||
let sops_config_path = if $sops_cfg_path_result.exit_code == 0 { $sops_cfg_path_result.stdout } else { "" }
|
||||
if ($sops_config_path | is-not-empty) {
|
||||
$result = ($result | str replace --all "{{sops.config_path}}" $sops_config_path)
|
||||
}
|
||||
@ -1231,19 +1268,22 @@ def interpolate-provider-refs [
|
||||
mut result = $text
|
||||
|
||||
# AWS provider region
|
||||
let aws_region = ($config | try { get providers.aws.region } catch { ""})
|
||||
let aws_region_result = (do { $config | get providers.aws.region } | complete)
|
||||
let aws_region = if $aws_region_result.exit_code == 0 { $aws_region_result.stdout } else { "" }
|
||||
if ($aws_region | is-not-empty) {
|
||||
$result = ($result | str replace --all "{{providers.aws.region}}" $aws_region)
|
||||
}
|
||||
|
||||
# Default provider
|
||||
let default_provider = ($config | try { get providers.default } catch { ""})
|
||||
let default_prov_result = (do { $config | get providers.default } | complete)
|
||||
let default_provider = if $default_prov_result.exit_code == 0 { $default_prov_result.stdout } else { "" }
|
||||
if ($default_provider | is-not-empty) {
|
||||
$result = ($result | str replace --all "{{providers.default}}" $default_provider)
|
||||
}
|
||||
|
||||
# UpCloud zone
|
||||
let upcloud_zone = ($config | try { get providers.upcloud.zone } catch { ""})
|
||||
let upcloud_zone_result = (do { $config | get providers.upcloud.zone } | complete)
|
||||
let upcloud_zone = if $upcloud_zone_result.exit_code == 0 { $upcloud_zone_result.stdout } else { "" }
|
||||
if ($upcloud_zone | is-not-empty) {
|
||||
$result = ($result | str replace --all "{{providers.upcloud.zone}}" $upcloud_zone)
|
||||
}
|
||||
@ -1260,13 +1300,15 @@ def interpolate-advanced-features [
|
||||
|
||||
# Function call: {{path.join(paths.base, "custom")}}
|
||||
if ($result | str contains "{{path.join(paths.base") {
|
||||
let base_path = ($config | try { get paths.base } catch { ""})
|
||||
let base_path_result = (do { $config | get paths.base } | complete)
|
||||
let base_path = if $base_path_result.exit_code == 0 { $base_path_result.stdout } else { "" }
|
||||
# Simple implementation for path.join with base path
|
||||
$result = ($result | str replace --regex "\\{\\{path\\.join\\(paths\\.base,\\s*\"([^\"]+)\"\\)\\}\\}" $"($base_path)/$1")
|
||||
}
|
||||
|
||||
# Environment-aware paths: {{paths.base.${env}}}
|
||||
let current_env = ($config | try { get current_environment } catch { "dev"})
|
||||
let current_env_result = (do { $config | get current_environment } | complete)
|
||||
let current_env = if $current_env_result.exit_code == 0 { $current_env_result.stdout } else { "dev" }
|
||||
$result = ($result | str replace --all "{{paths.base.${env}}}" $"{{paths.base}}.($current_env)")
|
||||
|
||||
$result
|
||||
@ -1542,7 +1584,8 @@ export def secure-interpolation [
|
||||
}
|
||||
|
||||
# Apply interpolation with depth limiting
|
||||
let base_path = ($config | try { get paths.base } catch { ""})
|
||||
let base_path_sec_result = (do { $config | get paths.base } | complete)
|
||||
let base_path = if $base_path_sec_result.exit_code == 0 { $base_path_sec_result.stdout } else { "" }
|
||||
if ($base_path | is-not-empty) {
|
||||
interpolate-with-depth-limit $config $base_path $max_depth
|
||||
} else {
|
||||
@ -1880,7 +1923,8 @@ export def detect-current-environment [] {
|
||||
export def get-available-environments [
|
||||
config: record
|
||||
] {
|
||||
let environments_section = ($config | try { get "environments" } catch { {} })
|
||||
let env_section_result = (do { $config | get "environments" } | complete)
|
||||
let environments_section = if $env_section_result.exit_code == 0 { $env_section_result.stdout } else { {} }
|
||||
$environments_section | columns
|
||||
}
|
||||
|
||||
@ -1928,7 +1972,8 @@ export def apply-environment-variable-overrides [
|
||||
}
|
||||
|
||||
for env_var in ($env_mappings | columns) {
|
||||
let env_value = ($env | try { get $env_var } catch { null })
|
||||
let env_map_result = (do { $env | get $env_var } | complete)
|
||||
let env_value = if $env_map_result.exit_code == 0 { $env_map_result.stdout } else { null }
|
||||
if ($env_value | is-not-empty) {
|
||||
let mapping = ($env_mappings | get $env_var)
|
||||
let config_path = $mapping.path
|
||||
@ -1975,14 +2020,19 @@ def set-config-value [
|
||||
} else if ($path_parts | length) == 2 {
|
||||
let section = ($path_parts | first)
|
||||
let key = ($path_parts | last)
|
||||
let section_data = ($result | try { get $section } catch { {} })
|
||||
let immutable_result = $result
|
||||
let section_result = (do { $immutable_result | get $section } | complete)
|
||||
let section_data = if $section_result.exit_code == 0 { $section_result.stdout } else { {} }
|
||||
$result | upsert $section ($section_data | upsert $key $value)
|
||||
} else if ($path_parts | length) == 3 {
|
||||
let section = ($path_parts | first)
|
||||
let subsection = ($path_parts | get 1)
|
||||
let key = ($path_parts | last)
|
||||
let section_data = ($result | try { get $section } catch { {} })
|
||||
let subsection_data = ($section_data | try { get $subsection } catch { {} })
|
||||
let immutable_result = $result
|
||||
let section_result = (do { $immutable_result | get $section } | complete)
|
||||
let section_data = if $section_result.exit_code == 0 { $section_result.stdout } else { {} }
|
||||
let subsection_result = (do { $section_data | get $subsection } | complete)
|
||||
let subsection_data = if $subsection_result.exit_code == 0 { $subsection_result.stdout } else { {} }
|
||||
$result | upsert $section ($section_data | upsert $subsection ($subsection_data | upsert $key $value))
|
||||
} else {
|
||||
# For deeper nesting, use recursive approach
|
||||
@ -2001,7 +2051,8 @@ def set-config-value-recursive [
|
||||
} else {
|
||||
let current_key = ($path_parts | first)
|
||||
let remaining_parts = ($path_parts | skip 1)
|
||||
let current_section = ($config | try { get $current_key } catch { {} })
|
||||
let current_result = (do { $config | get $current_key } | complete)
|
||||
let current_section = if $current_result.exit_code == 0 { $current_result.stdout } else { {} }
|
||||
$config | upsert $current_key (set-config-value-recursive $current_section $remaining_parts $value)
|
||||
}
|
||||
}
|
||||
@ -2011,7 +2062,8 @@ def apply-user-context-overrides [
|
||||
config: record
|
||||
context: record
|
||||
] {
|
||||
let overrides = ($context | try { get overrides } catch { {} })
|
||||
let overrides_result = (do { $context | get overrides } | complete)
|
||||
let overrides = if $overrides_result.exit_code == 0 { $overrides_result.stdout } else { {} }
|
||||
|
||||
mut result = $config
|
||||
|
||||
@ -2032,7 +2084,8 @@ def apply-user-context-overrides [
|
||||
}
|
||||
|
||||
# Update last_used timestamp for the workspace
|
||||
let workspace_name = ($context | try { get workspace.name } catch { null })
|
||||
let ws_result = (do { $context | get workspace.name } | complete)
|
||||
let workspace_name = if $ws_result.exit_code == 0 { $ws_result.stdout } else { null }
|
||||
if ($workspace_name | is-not-empty) {
|
||||
update-workspace-last-used-internal $workspace_name
|
||||
}
|
||||
@ -2055,7 +2108,7 @@ def update-workspace-last-used-internal [workspace_name: string] {
|
||||
}
|
||||
|
||||
# Check if file is SOPS encrypted (inline to avoid circular import)
|
||||
def check-if-sops-encrypted [file_path: string]: nothing -> bool {
|
||||
def check-if-sops-encrypted [file_path: string] {
|
||||
if not ($file_path | path exists) {
|
||||
return false
|
||||
}
|
||||
@ -2071,7 +2124,7 @@ def check-if-sops-encrypted [file_path: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Decrypt SOPS file (inline to avoid circular import)
|
||||
def decrypt-sops-file [file_path: string]: nothing -> string {
|
||||
def decrypt-sops-file [file_path: string] {
|
||||
# Find SOPS config
|
||||
let sops_config = find-sops-config-path
|
||||
|
||||
@ -2090,7 +2143,7 @@ def decrypt-sops-file [file_path: string]: nothing -> string {
|
||||
}
|
||||
|
||||
# Find SOPS configuration file
|
||||
def find-sops-config-path []: nothing -> string {
|
||||
def find-sops-config-path [] {
|
||||
# Check common locations
|
||||
let locations = [
|
||||
".sops.yaml"
|
||||
|
||||
270
nulib/lib_provisioning/config/loader_refactored.nu
Normal file
270
nulib/lib_provisioning/config/loader_refactored.nu
Normal file
@ -0,0 +1,270 @@
|
||||
# Configuration Loader Orchestrator - Coordinates modular config loading system
|
||||
# NUSHELL 0.109 COMPLIANT - Using reduce --fold (Rule 3), do-complete (Rule 5), each (Rule 8)
|
||||
|
||||
use std log
|
||||
|
||||
# Import all specialized modules
|
||||
use ./cache/core.nu *
|
||||
use ./cache/metadata.nu *
|
||||
use ./cache/config_manager.nu *
|
||||
use ./cache/nickel.nu *
|
||||
use ./cache/sops.nu *
|
||||
use ./cache/final.nu *
|
||||
|
||||
use ./loaders/file_loader.nu *
|
||||
use ./validation/config_validator.nu *
|
||||
use ./interpolation/core.nu *
|
||||
|
||||
use ./helpers/workspace.nu *
|
||||
use ./helpers/merging.nu *
|
||||
use ./helpers/environment.nu *
|
||||
|
||||
# Main configuration loader orchestrator
|
||||
# Coordinates the full loading pipeline: detect → cache check → load → merge → validate → interpolate → cache → return
|
||||
export def load-provisioning-config [
|
||||
--debug = false # Enable debug logging
|
||||
--validate = false # Validate configuration
|
||||
--environment: string # Override environment (dev/prod/test)
|
||||
--skip-env-detection = false # Skip automatic environment detection
|
||||
--no-cache = false # Disable cache
|
||||
]: nothing -> record {
|
||||
if $debug {
|
||||
# log debug "Loading provisioning configuration..."
|
||||
}
|
||||
|
||||
# Step 1: Detect current environment
|
||||
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)"
|
||||
}
|
||||
|
||||
# Step 2: Get active workspace
|
||||
let active_workspace = (get-active-workspace)
|
||||
|
||||
# Step 3: Check final config cache (if enabled)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
# Step 4: Prepare config sources list
|
||||
let config_sources = (prepare-config-sources $active_workspace $debug)
|
||||
|
||||
# Step 5: Load and merge all config sources (Rule 3: using reduce --fold)
|
||||
let loaded_config = ($config_sources | reduce --fold {base: {}, user_context: {}} {|source, result|
|
||||
let format = ($source.format | default "auto")
|
||||
let config_data = (load-config-file $source.path $source.required $debug $format)
|
||||
|
||||
# Ensure config_data is a record
|
||||
let safe_config = if ($config_data | describe | str starts-with "record") {
|
||||
$config_data
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
|
||||
# Store user context separately for override processing
|
||||
if $source.name == "user-context" {
|
||||
$result | upsert user_context $safe_config
|
||||
} else if ($safe_config | is-not-empty) {
|
||||
if $debug {
|
||||
# log debug $"Loaded ($source.name) config"
|
||||
}
|
||||
$result | upsert base (deep-merge $result.base $safe_config)
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
})
|
||||
|
||||
# Step 6: Apply user context overrides
|
||||
let final_config = if (($loaded_config.user_context | columns | length) > 0) {
|
||||
apply-user-context-overrides $loaded_config.base $loaded_config.user_context
|
||||
} else {
|
||||
$loaded_config.base
|
||||
}
|
||||
|
||||
# Step 7: Apply environment-specific overrides
|
||||
let env_config = if ($current_environment | is-not-empty) {
|
||||
let env_result = (do { $final_config | get $"environments.($current_environment)" } | complete)
|
||||
if $env_result.exit_code == 0 { $env_result.stdout } else { {} }
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
|
||||
let with_env_overrides = if ($env_config | is-not-empty) {
|
||||
if $debug {
|
||||
# log debug $"Applying environment overrides for: ($current_environment)"
|
||||
}
|
||||
(deep-merge $final_config $env_config)
|
||||
} else {
|
||||
$final_config
|
||||
}
|
||||
|
||||
# Step 8: Apply environment variable overrides
|
||||
let with_env_vars = (apply-environment-variable-overrides $with_env_overrides $debug)
|
||||
|
||||
# Step 9: Add current environment to config
|
||||
let with_current_env = if ($current_environment | is-not-empty) {
|
||||
($with_env_vars | upsert "current_environment" $current_environment)
|
||||
} else {
|
||||
$with_env_vars
|
||||
}
|
||||
|
||||
# Step 10: Interpolate variables in configuration
|
||||
let interpolated = (interpolate-config $with_current_env)
|
||||
|
||||
# Step 11: Validate configuration (if requested)
|
||||
if $validate {
|
||||
let validation_result = (validate-config $interpolated --detailed false --strict false)
|
||||
# validate-config throws error if validation fails in non-detailed mode
|
||||
}
|
||||
|
||||
# Step 12: Cache final config (ignore errors)
|
||||
if (not $no_cache) and ($active_workspace | is-not-empty) {
|
||||
do {
|
||||
cache-final-config $interpolated $active_workspace $current_environment
|
||||
} | complete | ignore
|
||||
}
|
||||
|
||||
if $debug {
|
||||
# log debug "Configuration loading completed"
|
||||
}
|
||||
|
||||
# Step 13: Return final configuration
|
||||
$interpolated
|
||||
}
|
||||
|
||||
# Prepare list of configuration sources from workspace
|
||||
# Returns: list of {name, path, required, format} records
|
||||
def prepare-config-sources [active_workspace: any, debug: bool]: nothing -> list {
|
||||
if ($active_workspace | is-empty) {
|
||||
# Fallback: Try to find workspace from current directory
|
||||
prepare-fallback-sources debug $debug
|
||||
} else {
|
||||
prepare-workspace-sources $active_workspace $debug
|
||||
}
|
||||
}
|
||||
|
||||
# Prepare config sources from active workspace directory
|
||||
def prepare-workspace-sources [workspace: record, debug: bool]: nothing -> list {
|
||||
let config_dir = ($workspace.path | path join "config")
|
||||
let generated_workspace = ($config_dir | path join "generated" | path join "workspace.toml")
|
||||
let ncl_config = ($config_dir | path join "config.ncl")
|
||||
let nickel_config = ($config_dir | path join "provisioning.ncl")
|
||||
let yaml_config = ($config_dir | path join "provisioning.yaml")
|
||||
|
||||
# Priority: Generated TOML > config.ncl > provisioning.ncl > provisioning.yaml
|
||||
let workspace_source = if ($generated_workspace | path exists) {
|
||||
{name: "workspace", path: $generated_workspace, required: true, format: "toml"}
|
||||
} else if ($ncl_config | path exists) {
|
||||
{name: "workspace", path: $ncl_config, required: true, format: "ncl"}
|
||||
} else if ($nickel_config | path exists) {
|
||||
{name: "workspace", path: $nickel_config, required: true, format: "nickel"}
|
||||
} else if ($yaml_config | path exists) {
|
||||
{name: "workspace", path: $yaml_config, required: true, format: "yaml"}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
# Load provider configs (Rule 8: using each)
|
||||
let provider_sources = (
|
||||
let gen_dir = ($workspace.path | path join "config" | path join "generated" | path join "providers")
|
||||
let man_dir = ($workspace.path | path join "config" | path join "providers")
|
||||
let provider_dir = if ($gen_dir | path exists) { $gen_dir } else { $man_dir }
|
||||
|
||||
if ($provider_dir | path exists) {
|
||||
do {
|
||||
ls $provider_dir | where type == file and ($it.name | str ends-with '.toml') | each {|f|
|
||||
{
|
||||
name: $"provider-($f.name | str replace '.toml' '')",
|
||||
path: $f.name,
|
||||
required: false,
|
||||
format: "toml"
|
||||
}
|
||||
}
|
||||
} | complete | if $in.exit_code == 0 { $in.stdout } else { [] }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
# Load platform configs (Rule 8: using each)
|
||||
let platform_sources = (
|
||||
let gen_dir = ($workspace.path | path join "config" | path join "generated" | path join "platform")
|
||||
let man_dir = ($workspace.path | path join "config" | path join "platform")
|
||||
let platform_dir = if ($gen_dir | path exists) { $gen_dir } else { $man_dir }
|
||||
|
||||
if ($platform_dir | path exists) {
|
||||
do {
|
||||
ls $platform_dir | where type == file and ($it.name | str ends-with '.toml') | each {|f|
|
||||
{
|
||||
name: $"platform-($f.name | str replace '.toml' '')",
|
||||
path: $f.name,
|
||||
required: false,
|
||||
format: "toml"
|
||||
}
|
||||
}
|
||||
} | complete | if $in.exit_code == 0 { $in.stdout } else { [] }
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
# Load user context (highest priority before env vars)
|
||||
let user_context_source = (
|
||||
let user_dir = ([$env.HOME "Library" "Application Support" "provisioning"] | path join)
|
||||
let user_context = ([$user_dir $"ws_($workspace.name).yaml"] | path join)
|
||||
if ($user_context | path exists) {
|
||||
[{name: "user-context", path: $user_context, required: false, format: "yaml"}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
)
|
||||
|
||||
# Combine all sources (Rule 3: immutable appending)
|
||||
if ($workspace_source | is-not-empty) {
|
||||
([$workspace_source] | append $provider_sources | append $platform_sources | append $user_context_source)
|
||||
} else {
|
||||
([] | append $provider_sources | append $platform_sources | append $user_context_source)
|
||||
}
|
||||
}
|
||||
|
||||
# Prepare config sources from current directory (fallback when no workspace active)
|
||||
def prepare-fallback-sources [debug: bool]: nothing -> list {
|
||||
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")
|
||||
|
||||
if ($ncl_config | path exists) {
|
||||
[{name: "workspace", path: $ncl_config, required: true, format: "ncl"}]
|
||||
} else if ($nickel_config | path exists) {
|
||||
[{name: "workspace", path: $nickel_config, required: true, format: "nickel"}]
|
||||
} else if ($yaml_config | path exists) {
|
||||
[{name: "workspace", path: $yaml_config, required: true, format: "yaml"}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
}
|
||||
|
||||
# Apply user context overrides with proper priority
|
||||
def apply-user-context-overrides [config: record, user_context: record]: nothing -> record {
|
||||
# User context is highest config priority (before env vars)
|
||||
deep-merge $config $user_context
|
||||
}
|
||||
|
||||
# Export public functions from load-provisioning-config for backward compatibility
|
||||
export use ./loaders/file_loader.nu [load-config-file]
|
||||
export use ./validation/config_validator.nu [validate-config, validate-config-structure, validate-path-values, validate-data-types, validate-semantic-rules, validate-file-existence]
|
||||
export use ./interpolation/core.nu [interpolate-config, interpolate-string, validate-interpolation, get-config-value]
|
||||
export use ./helpers/workspace.nu [get-active-workspace, get-project-root, update-workspace-last-used]
|
||||
export use ./helpers/merging.nu [deep-merge]
|
||||
export use ./helpers/environment.nu [detect-current-environment, get-available-environments, apply-environment-variable-overrides, validate-environment]
|
||||
330
nulib/lib_provisioning/config/loaders/file_loader.nu
Normal file
330
nulib/lib_provisioning/config/loaders/file_loader.nu
Normal file
@ -0,0 +1,330 @@
|
||||
# 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 {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
# Core configuration functionality
|
||||
export use loader.nu *
|
||||
export use accessor.nu *
|
||||
export use accessor_generated.nu * # Schema-driven generated accessors
|
||||
export use migration.nu *
|
||||
|
||||
# Encryption functionality
|
||||
|
||||
@ -1,180 +1,314 @@
|
||||
# Validate config against schema
|
||||
export def validate-config-with-schema [
|
||||
config: record
|
||||
schema_file: string
|
||||
] {
|
||||
if not ($schema_file | path exists) {
|
||||
error make { msg: $"Schema file not found: ($schema_file)" }
|
||||
}
|
||||
# Schema Validator
|
||||
# Handles validation of infrastructure configurations against defined schemas
|
||||
|
||||
let schema = (open $schema_file | from toml)
|
||||
# Server configuration schema validation
|
||||
export def validate_server_schema [config: record] {
|
||||
mut issues = []
|
||||
|
||||
mut errors = []
|
||||
mut warnings = []
|
||||
# Required fields for server configuration
|
||||
let required_fields = [
|
||||
"hostname"
|
||||
"provider"
|
||||
"zone"
|
||||
"plan"
|
||||
]
|
||||
|
||||
# Validate required fields
|
||||
if ($schema | get -i required | is-not-empty) {
|
||||
for field in ($schema.required | default []) {
|
||||
if ($config | get -i $field | is-empty) {
|
||||
$errors = ($errors | append {
|
||||
for field in $required_fields {
|
||||
if not ($config | try { get $field } catch { null } | is-not-empty) {
|
||||
$issues = ($issues | append {
|
||||
field: $field
|
||||
type: "missing_required"
|
||||
message: $"Required field missing: ($field)"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Validate field types
|
||||
if ($schema | get -i fields | is-not-empty) {
|
||||
for field_name in ($schema.fields | columns) {
|
||||
let field_schema = ($schema.fields | get $field_name)
|
||||
let field_value = ($config | get -i $field_name)
|
||||
|
||||
if ($field_value | is-not-empty) {
|
||||
let expected_type = ($field_schema | get -i type)
|
||||
let actual_type = ($field_value | describe)
|
||||
|
||||
if ($expected_type | is-not-empty) and $expected_type != $actual_type {
|
||||
$errors = ($errors | append {
|
||||
field: $field_name
|
||||
type: "type_mismatch"
|
||||
expected: $expected_type
|
||||
actual: $actual_type
|
||||
message: $"Field ($field_name) type mismatch: expected ($expected_type), got ($actual_type)"
|
||||
})
|
||||
}
|
||||
|
||||
# Validate enum values
|
||||
if ($field_schema | get -i enum | is-not-empty) {
|
||||
let valid_values = ($field_schema.enum)
|
||||
if not ($field_value in $valid_values) {
|
||||
$errors = ($errors | append {
|
||||
field: $field_name
|
||||
type: "invalid_enum"
|
||||
value: $field_value
|
||||
valid_values: $valid_values
|
||||
message: $"Field ($field_name) must be one of: ($valid_values | str join ', ')"
|
||||
message: $"Required field '($field)' is missing or empty"
|
||||
severity: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Validate min/max for numbers
|
||||
if ($actual_type == "int" or $actual_type == "float") {
|
||||
if ($field_schema | get -i min | is-not-empty) {
|
||||
let min_val = ($field_schema.min)
|
||||
if $field_value < $min_val {
|
||||
$errors = ($errors | append {
|
||||
field: $field_name
|
||||
type: "value_too_small"
|
||||
value: $field_value
|
||||
min: $min_val
|
||||
message: $"Field ($field_name) must be >= ($min_val)"
|
||||
# Validate specific field formats
|
||||
if ($config | try { get hostname } catch { null } | is-not-empty) {
|
||||
let hostname = ($config | get hostname)
|
||||
if not ($hostname =~ '^[a-z0-9][a-z0-9\-]*[a-z0-9]$') {
|
||||
$issues = ($issues | append {
|
||||
field: "hostname"
|
||||
message: "Hostname must contain only lowercase letters, numbers, and hyphens"
|
||||
severity: "warning"
|
||||
current_value: $hostname
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if ($field_schema | get -i max | is-not-empty) {
|
||||
let max_val = ($field_schema.max)
|
||||
if $field_value > $max_val {
|
||||
$errors = ($errors | append {
|
||||
field: $field_name
|
||||
type: "value_too_large"
|
||||
value: $field_value
|
||||
max: $max_val
|
||||
message: $"Field ($field_name) must be <= ($max_val)"
|
||||
})
|
||||
}
|
||||
}
|
||||
# Validate provider-specific requirements
|
||||
if ($config | try { get provider } catch { null } | is-not-empty) {
|
||||
let provider = ($config | get provider)
|
||||
let provider_validation = (validate_provider_config $provider $config)
|
||||
$issues = ($issues | append $provider_validation.issues)
|
||||
}
|
||||
|
||||
# Validate pattern for strings
|
||||
if $actual_type == "string" and ($field_schema | get -i pattern | is-not-empty) {
|
||||
let pattern = ($field_schema.pattern)
|
||||
if not ($field_value =~ $pattern) {
|
||||
$errors = ($errors | append {
|
||||
field: $field_name
|
||||
type: "pattern_mismatch"
|
||||
value: $field_value
|
||||
pattern: $pattern
|
||||
message: $"Field ($field_name) does not match pattern: ($pattern)"
|
||||
# Validate network configuration
|
||||
if ($config | try { get network_private_ip } catch { null } | is-not-empty) {
|
||||
let ip = ($config | get network_private_ip)
|
||||
let ip_validation = (validate_ip_address $ip)
|
||||
if not $ip_validation.valid {
|
||||
$issues = ($issues | append {
|
||||
field: "network_private_ip"
|
||||
message: $ip_validation.message
|
||||
severity: "error"
|
||||
current_value: $ip
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Check for deprecated fields
|
||||
if ($schema | get -i deprecated | is-not-empty) {
|
||||
for deprecated_field in ($schema.deprecated | default []) {
|
||||
if ($config | get -i $deprecated_field | is-not-empty) {
|
||||
let replacement = ($schema.deprecated_replacements | get -i $deprecated_field | default "unknown")
|
||||
$warnings = ($warnings | append {
|
||||
field: $deprecated_field
|
||||
type: "deprecated"
|
||||
replacement: $replacement
|
||||
message: $"Field ($deprecated_field) is deprecated. Use ($replacement) instead."
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
valid: (($errors | length) == 0)
|
||||
errors: $errors
|
||||
warnings: $warnings
|
||||
valid: (($issues | where severity == "error" | length) == 0)
|
||||
issues: $issues
|
||||
}
|
||||
}
|
||||
|
||||
# Validate provider config
|
||||
export def validate-provider-config [
|
||||
provider_name: string
|
||||
config: record
|
||||
] {
|
||||
let schema_file = $"/Users/Akasha/project-provisioning/provisioning/extensions/providers/($provider_name)/config.schema.toml"
|
||||
validate-config-with-schema $config $schema_file
|
||||
# Provider-specific configuration validation
|
||||
export def validate_provider_config [provider: string, config: record] {
|
||||
mut issues = []
|
||||
|
||||
match $provider {
|
||||
"upcloud" => {
|
||||
# UpCloud specific validations
|
||||
let required_upcloud_fields = ["ssh_key_path", "storage_os"]
|
||||
for field in $required_upcloud_fields {
|
||||
if not ($config | try { get $field } catch { null } | is-not-empty) {
|
||||
$issues = ($issues | append {
|
||||
field: $field
|
||||
message: $"UpCloud provider requires '($field)' field"
|
||||
severity: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Validate platform service config
|
||||
export def validate-platform-config [
|
||||
service_name: string
|
||||
config: record
|
||||
] {
|
||||
let schema_file = $"/Users/Akasha/project-provisioning/provisioning/platform/($service_name)/config.schema.toml"
|
||||
validate-config-with-schema $config $schema_file
|
||||
# Validate UpCloud zones
|
||||
let valid_zones = ["es-mad1", "fi-hel1", "fi-hel2", "nl-ams1", "sg-sin1", "uk-lon1", "us-chi1", "us-nyc1", "de-fra1"]
|
||||
let zone = ($config | try { get zone } catch { null })
|
||||
if ($zone | is-not-empty) and ($zone not-in $valid_zones) {
|
||||
$issues = ($issues | append {
|
||||
field: "zone"
|
||||
message: $"Invalid UpCloud zone: ($zone)"
|
||||
severity: "error"
|
||||
current_value: $zone
|
||||
suggested_values: $valid_zones
|
||||
})
|
||||
}
|
||||
}
|
||||
"aws" => {
|
||||
# AWS specific validations
|
||||
let required_aws_fields = ["instance_type", "ami_id"]
|
||||
for field in $required_aws_fields {
|
||||
if not ($config | try { get $field } catch { null } | is-not-empty) {
|
||||
$issues = ($issues | append {
|
||||
field: $field
|
||||
message: $"AWS provider requires '($field)' field"
|
||||
severity: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
"local" => {
|
||||
# Local provider specific validations
|
||||
# Generally more lenient
|
||||
}
|
||||
_ => {
|
||||
$issues = ($issues | append {
|
||||
field: "provider"
|
||||
message: $"Unknown provider: ($provider)"
|
||||
severity: "error"
|
||||
current_value: $provider
|
||||
suggested_values: ["upcloud", "aws", "local"]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Validate KMS config
|
||||
export def validate-kms-config [config: record] {
|
||||
let schema_file = "/Users/Akasha/project-provisioning/provisioning/core/services/kms/config.schema.toml"
|
||||
validate-config-with-schema $config $schema_file
|
||||
{ issues: $issues }
|
||||
}
|
||||
|
||||
# Validate workspace config
|
||||
export def validate-workspace-config [config: record] {
|
||||
let schema_file = "/Users/Akasha/project-provisioning/provisioning/config/workspace.schema.toml"
|
||||
validate-config-with-schema $config $schema_file
|
||||
# Network configuration validation
|
||||
export def validate_network_config [config: record] {
|
||||
mut issues = []
|
||||
|
||||
# Validate CIDR blocks
|
||||
if ($config | try { get priv_cidr_block } catch { null } | is-not-empty) {
|
||||
let cidr = ($config | get priv_cidr_block)
|
||||
let cidr_validation = (validate_cidr_block $cidr)
|
||||
if not $cidr_validation.valid {
|
||||
$issues = ($issues | append {
|
||||
field: "priv_cidr_block"
|
||||
message: $cidr_validation.message
|
||||
severity: "error"
|
||||
current_value: $cidr
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Pretty print validation results
|
||||
export def print-validation-results [result: record] {
|
||||
if $result.valid {
|
||||
print "✅ Validation passed"
|
||||
# Check for IP conflicts
|
||||
if ($config | try { get network_private_ip } catch { null } | is-not-empty) and ($config | try { get priv_cidr_block } catch { null } | is-not-empty) {
|
||||
let ip = ($config | get network_private_ip)
|
||||
let cidr = ($config | get priv_cidr_block)
|
||||
|
||||
if not (ip_in_cidr $ip $cidr) {
|
||||
$issues = ($issues | append {
|
||||
field: "network_private_ip"
|
||||
message: $"IP ($ip) is not within CIDR block ($cidr)"
|
||||
severity: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
valid: (($issues | where severity == "error" | length) == 0)
|
||||
issues: $issues
|
||||
}
|
||||
}
|
||||
|
||||
# TaskServ configuration validation
|
||||
export def validate_taskserv_schema [taskserv: record] {
|
||||
mut issues = []
|
||||
|
||||
let required_fields = ["name", "install_mode"]
|
||||
|
||||
for field in $required_fields {
|
||||
if not ($taskserv | try { get $field } catch { null } | is-not-empty) {
|
||||
$issues = ($issues | append {
|
||||
field: $field
|
||||
message: $"Required taskserv field '($field)' is missing"
|
||||
severity: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Validate install mode
|
||||
let valid_install_modes = ["library", "container", "binary"]
|
||||
let install_mode = ($taskserv | try { get install_mode } catch { null })
|
||||
if ($install_mode | is-not-empty) and ($install_mode not-in $valid_install_modes) {
|
||||
$issues = ($issues | append {
|
||||
field: "install_mode"
|
||||
message: $"Invalid install_mode: ($install_mode)"
|
||||
severity: "error"
|
||||
current_value: $install_mode
|
||||
suggested_values: $valid_install_modes
|
||||
})
|
||||
}
|
||||
|
||||
# Validate taskserv name exists
|
||||
let taskserv_name = ($taskserv | try { get name } catch { null })
|
||||
if ($taskserv_name | is-not-empty) {
|
||||
let taskserv_exists = (taskserv_definition_exists $taskserv_name)
|
||||
if not $taskserv_exists {
|
||||
$issues = ($issues | append {
|
||||
field: "name"
|
||||
message: $"TaskServ definition not found: ($taskserv_name)"
|
||||
severity: "warning"
|
||||
current_value: $taskserv_name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
valid: (($issues | where severity == "error" | length) == 0)
|
||||
issues: $issues
|
||||
}
|
||||
}
|
||||
|
||||
# Helper validation functions
|
||||
|
||||
export def validate_ip_address [ip: string] {
|
||||
# Basic IP address validation (IPv4)
|
||||
if ($ip =~ '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$') {
|
||||
let parts = ($ip | split row ".")
|
||||
let valid_parts = ($parts | all {|part|
|
||||
let num = ($part | into int)
|
||||
$num >= 0 and $num <= 255
|
||||
})
|
||||
|
||||
if $valid_parts {
|
||||
{ valid: true, message: "" }
|
||||
} else {
|
||||
print "❌ Validation failed"
|
||||
print ""
|
||||
print "Errors:"
|
||||
for error in $result.errors {
|
||||
print $" • ($error.message)"
|
||||
{ valid: false, message: "IP address octets must be between 0 and 255" }
|
||||
}
|
||||
} else {
|
||||
{ valid: false, message: "Invalid IP address format" }
|
||||
}
|
||||
}
|
||||
|
||||
if ($result.warnings | length) > 0 {
|
||||
print ""
|
||||
print "⚠️ Warnings:"
|
||||
for warning in $result.warnings {
|
||||
print $" • ($warning.message)"
|
||||
export def validate_cidr_block [cidr: string] {
|
||||
if ($cidr =~ '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$') {
|
||||
let parts = ($cidr | split row "/")
|
||||
let ip_part = ($parts | get 0)
|
||||
let prefix = ($parts | get 1 | into int)
|
||||
|
||||
let ip_valid = (validate_ip_address $ip_part)
|
||||
if not $ip_valid.valid {
|
||||
return $ip_valid
|
||||
}
|
||||
|
||||
if $prefix >= 0 and $prefix <= 32 {
|
||||
{ valid: true, message: "" }
|
||||
} else {
|
||||
{ valid: false, message: "CIDR prefix must be between 0 and 32" }
|
||||
}
|
||||
} else {
|
||||
{ valid: false, message: "Invalid CIDR block format (should be x.x.x.x/y)" }
|
||||
}
|
||||
}
|
||||
|
||||
export def ip_in_cidr [ip: string, cidr: string] {
|
||||
# Simplified IP in CIDR check
|
||||
# This is a basic implementation - a more robust version would use proper IP arithmetic
|
||||
let cidr_parts = ($cidr | split row "/")
|
||||
let network = ($cidr_parts | get 0)
|
||||
let prefix = ($cidr_parts | get 1 | into int)
|
||||
|
||||
# For basic validation, check if IP starts with the same network portion
|
||||
# This is simplified and should be enhanced for production use
|
||||
if $prefix >= 24 {
|
||||
let network_base = ($network | split row "." | take 3 | str join ".")
|
||||
let ip_base = ($ip | split row "." | take 3 | str join ".")
|
||||
$network_base == $ip_base
|
||||
} else {
|
||||
# For smaller networks, more complex logic would be needed
|
||||
true # Simplified for now
|
||||
}
|
||||
}
|
||||
|
||||
export def taskserv_definition_exists [name: string] {
|
||||
# Check if taskserv definition exists in the system
|
||||
let taskserv_path = $"taskservs/($name)"
|
||||
($taskserv_path | path exists)
|
||||
}
|
||||
|
||||
# Schema definitions for different resource types
|
||||
export def get_server_schema [] {
|
||||
{
|
||||
required_fields: ["hostname", "provider", "zone", "plan"]
|
||||
optional_fields: [
|
||||
"title", "labels", "ssh_key_path", "storage_os",
|
||||
"network_private_ip", "priv_cidr_block", "time_zone",
|
||||
"taskservs", "storages"
|
||||
]
|
||||
field_types: {
|
||||
hostname: "string"
|
||||
provider: "string"
|
||||
zone: "string"
|
||||
plan: "string"
|
||||
network_private_ip: "ip_address"
|
||||
priv_cidr_block: "cidr"
|
||||
taskservs: "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export def get_taskserv_schema [] {
|
||||
{
|
||||
required_fields: ["name", "install_mode"]
|
||||
optional_fields: ["profile", "target_save_path"]
|
||||
field_types: {
|
||||
name: "string"
|
||||
install_mode: "string"
|
||||
profile: "string"
|
||||
target_save_path: "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
383
nulib/lib_provisioning/config/validation/config_validator.nu
Normal file
383
nulib/lib_provisioning/config/validation/config_validator.nu
Normal file
@ -0,0 +1,383 @@
|
||||
# Configuration validation - Checks config structure, types, paths, and semantic rules
|
||||
# NUSHELL 0.109 COMPLIANT - Using reduce --fold (Rule 3), do-complete (Rule 5), each (Rule 8)
|
||||
|
||||
# Validate configuration structure - checks required sections exist
|
||||
export def validate-config-structure [config: record]: nothing -> record {
|
||||
let required_sections = ["core", "paths", "debug", "sops"]
|
||||
|
||||
# Use reduce --fold to collect errors (Rule 3: no mutable variables)
|
||||
let validation_result = ($required_sections | reduce --fold {errors: [], warnings: []} {|section, result|
|
||||
let section_result = (do { $config | get $section } | complete)
|
||||
let section_value = if $section_result.exit_code == 0 { $section_result.stdout } else { null }
|
||||
|
||||
if ($section_value | is-empty) {
|
||||
$result | upsert errors ($result.errors | append {
|
||||
type: "missing_section",
|
||||
severity: "error",
|
||||
section: $section,
|
||||
message: $"Missing required configuration section: ($section)"
|
||||
})
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
valid: (($validation_result.errors | length) == 0),
|
||||
errors: $validation_result.errors,
|
||||
warnings: $validation_result.warnings
|
||||
}
|
||||
}
|
||||
|
||||
# Validate path values - checks paths exist and are absolute
|
||||
export def validate-path-values [config: record]: nothing -> record {
|
||||
let required_paths = ["base", "providers", "taskservs", "clusters"]
|
||||
|
||||
let paths_result = (do { $config | get paths } | complete)
|
||||
let paths = if $paths_result.exit_code == 0 { $paths_result.stdout } else { {} }
|
||||
|
||||
# Collect validation errors and warnings (Rule 3: using reduce --fold)
|
||||
let validation_result = ($required_paths | reduce --fold {errors: [], warnings: []} {|path_name, result|
|
||||
let path_result = (do { $paths | get $path_name } | complete)
|
||||
let path_value = if $path_result.exit_code == 0 { $path_result.stdout } else { null }
|
||||
|
||||
if ($path_value | is-empty) {
|
||||
$result | upsert errors ($result.errors | append {
|
||||
type: "missing_path",
|
||||
severity: "error",
|
||||
path: $path_name,
|
||||
message: $"Missing required path: paths.($path_name)"
|
||||
})
|
||||
} else {
|
||||
# Check if path is absolute
|
||||
let abs_result = if not ($path_value | str starts-with "/") {
|
||||
$result | upsert warnings ($result.warnings | append {
|
||||
type: "relative_path",
|
||||
severity: "warning",
|
||||
path: $path_name,
|
||||
value: $path_value,
|
||||
message: $"Path paths.($path_name) should be absolute, got: ($path_value)"
|
||||
})
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
|
||||
# Check if base path exists (critical for system operation)
|
||||
if $path_name == "base" and not ($path_value | path exists) {
|
||||
$abs_result | upsert errors ($abs_result.errors | append {
|
||||
type: "path_not_exists",
|
||||
severity: "error",
|
||||
path: $path_name,
|
||||
value: $path_value,
|
||||
message: $"Base path does not exist: ($path_value)"
|
||||
})
|
||||
} else {
|
||||
$abs_result
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
valid: (($validation_result.errors | length) == 0),
|
||||
errors: $validation_result.errors,
|
||||
warnings: $validation_result.warnings
|
||||
}
|
||||
}
|
||||
|
||||
# Validate data types - checks configuration values have correct types
|
||||
export def validate-data-types [config: record]: nothing -> record {
|
||||
let type_checks = [
|
||||
{ field: "core.version", expected: "string", validator: {|v|
|
||||
let parts = ($v | split row ".")
|
||||
($parts | length) >= 3
|
||||
}},
|
||||
{ field: "debug.enabled", expected: "bool" },
|
||||
{ field: "debug.metadata", expected: "bool" },
|
||||
{ field: "sops.use_sops", expected: "bool" }
|
||||
]
|
||||
|
||||
# Validate each type check (Rule 3: using reduce --fold, Rule 8: using each)
|
||||
let validation_result = ($type_checks | reduce --fold {errors: [], warnings: []} {|check, result|
|
||||
let field_result = (do {
|
||||
let parts = ($check.field | split row ".")
|
||||
if ($parts | length) == 2 {
|
||||
$config | get ($parts | first) | get ($parts | last)
|
||||
} else {
|
||||
$config | get $check.field
|
||||
}
|
||||
} | complete)
|
||||
|
||||
let value = if $field_result.exit_code == 0 { $field_result.stdout } else { null }
|
||||
|
||||
if ($value | is-empty) {
|
||||
$result
|
||||
} else {
|
||||
let actual_type = ($value | describe)
|
||||
let type_matches = if ($check.expected == "bool") {
|
||||
$actual_type == "bool"
|
||||
} else if ($check.expected == "string") {
|
||||
$actual_type == "string"
|
||||
} else {
|
||||
$actual_type == $check.expected
|
||||
}
|
||||
|
||||
if not $type_matches {
|
||||
$result | upsert errors ($result.errors | append {
|
||||
type: "invalid_type",
|
||||
severity: "error",
|
||||
field: $check.field,
|
||||
value: $value,
|
||||
expected: $check.expected,
|
||||
actual: $actual_type,
|
||||
message: $"($check.field) must be ($check.expected), got: ($actual_type)"
|
||||
})
|
||||
} else if ($check.validator? != null) {
|
||||
# Additional validation via closure (if provided)
|
||||
if (($check.validator | call $value)) {
|
||||
$result
|
||||
} else {
|
||||
$result | upsert errors ($result.errors | append {
|
||||
type: "invalid_value",
|
||||
severity: "error",
|
||||
field: $check.field,
|
||||
value: $value,
|
||||
message: $"($check.field) has invalid value: ($value)"
|
||||
})
|
||||
}
|
||||
} else {
|
||||
$result
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
valid: (($validation_result.errors | length) == 0),
|
||||
errors: $validation_result.errors,
|
||||
warnings: $validation_result.warnings
|
||||
}
|
||||
}
|
||||
|
||||
# Validate semantic rules - business logic validation
|
||||
export def validate-semantic-rules [config: record]: nothing -> record {
|
||||
let providers_result = (do { $config | get providers } | complete)
|
||||
let providers = if $providers_result.exit_code == 0 { $providers_result.stdout } else { {} }
|
||||
let default_result = (do { $providers | get default } | complete)
|
||||
let default_provider = if $default_result.exit_code == 0 { $default_result.stdout } else { null }
|
||||
|
||||
# Validate provider
|
||||
let provider_check = if ($default_provider | is-not-empty) {
|
||||
let valid_providers = ["aws", "upcloud", "local"]
|
||||
if ($default_provider in $valid_providers) {
|
||||
{errors: [], warnings: []}
|
||||
} else {
|
||||
{
|
||||
errors: [{
|
||||
type: "invalid_provider",
|
||||
severity: "error",
|
||||
field: "providers.default",
|
||||
value: $default_provider,
|
||||
valid_options: $valid_providers,
|
||||
message: $"Invalid default provider: ($default_provider)"
|
||||
}],
|
||||
warnings: []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{errors: [], warnings: []}
|
||||
}
|
||||
|
||||
# Validate log level
|
||||
let log_level_result = (do { $config | get debug.log_level } | complete)
|
||||
let log_level = if $log_level_result.exit_code == 0 { $log_level_result.stdout } else { null }
|
||||
|
||||
let log_check = if ($log_level | is-not-empty) {
|
||||
let valid_levels = ["trace", "debug", "info", "warn", "error"]
|
||||
if ($log_level in $valid_levels) {
|
||||
{errors: [], warnings: []}
|
||||
} else {
|
||||
{
|
||||
errors: [],
|
||||
warnings: [{
|
||||
type: "invalid_log_level",
|
||||
severity: "warning",
|
||||
field: "debug.log_level",
|
||||
value: $log_level,
|
||||
valid_options: $valid_levels,
|
||||
message: $"Invalid log level: ($log_level)"
|
||||
}]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{errors: [], warnings: []}
|
||||
}
|
||||
|
||||
# Validate output format
|
||||
let output_result = (do { $config | get output.format } | complete)
|
||||
let output_format = if $output_result.exit_code == 0 { $output_result.stdout } else { null }
|
||||
|
||||
let format_check = if ($output_format | is-not-empty) {
|
||||
let valid_formats = ["json", "yaml", "toml", "text"]
|
||||
if ($output_format in $valid_formats) {
|
||||
{errors: [], warnings: []}
|
||||
} else {
|
||||
{
|
||||
errors: [],
|
||||
warnings: [{
|
||||
type: "invalid_output_format",
|
||||
severity: "warning",
|
||||
field: "output.format",
|
||||
value: $output_format,
|
||||
valid_options: $valid_formats,
|
||||
message: $"Invalid output format: ($output_format)"
|
||||
}]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{errors: [], warnings: []}
|
||||
}
|
||||
|
||||
# Combine all semantic checks (Rule 3: immutable combination)
|
||||
let all_errors = (
|
||||
$provider_check.errors | append $log_check.errors | append $format_check.errors
|
||||
)
|
||||
let all_warnings = (
|
||||
$provider_check.warnings | append $log_check.warnings | append $format_check.warnings
|
||||
)
|
||||
|
||||
{
|
||||
valid: (($all_errors | length) == 0),
|
||||
errors: $all_errors,
|
||||
warnings: $all_warnings
|
||||
}
|
||||
}
|
||||
|
||||
# Validate file existence - checks referenced files exist
|
||||
export def validate-file-existence [config: record]: nothing -> record {
|
||||
# Check SOPS configuration file
|
||||
let sops_cfg_result = (do { $config | get sops.config_path } | complete)
|
||||
let sops_config = if $sops_cfg_result.exit_code == 0 { $sops_cfg_result.stdout } else { null }
|
||||
|
||||
let sops_config_check = if ($sops_config | is-not-empty) and not ($sops_config | path exists) {
|
||||
[{
|
||||
type: "missing_sops_config",
|
||||
severity: "warning",
|
||||
field: "sops.config_path",
|
||||
value: $sops_config,
|
||||
message: $"SOPS config file not found: ($sops_config)"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Check SOPS key files
|
||||
let key_result = (do { $config | get sops.key_search_paths } | complete)
|
||||
let key_paths = if $key_result.exit_code == 0 { $key_result.stdout } else { [] }
|
||||
|
||||
let key_found = ($key_paths
|
||||
| any {|key_path|
|
||||
let expanded_path = ($key_path | str replace "~" $env.HOME)
|
||||
($expanded_path | path exists)
|
||||
}
|
||||
)
|
||||
|
||||
let sops_key_check = if not $key_found and ($key_paths | length) > 0 {
|
||||
[{
|
||||
type: "missing_sops_keys",
|
||||
severity: "warning",
|
||||
field: "sops.key_search_paths",
|
||||
value: $key_paths,
|
||||
message: $"No SOPS key files found in search paths"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Check critical configuration files
|
||||
let settings_result = (do { $config | get paths.files.settings } | complete)
|
||||
let settings_file = if $settings_result.exit_code == 0 { $settings_result.stdout } else { null }
|
||||
|
||||
let settings_check = if ($settings_file | is-not-empty) and not ($settings_file | path exists) {
|
||||
[{
|
||||
type: "missing_settings_file",
|
||||
severity: "error",
|
||||
field: "paths.files.settings",
|
||||
value: $settings_file,
|
||||
message: $"Settings file not found: ($settings_file)"
|
||||
}]
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
# Combine all checks (Rule 3: immutable combination)
|
||||
let all_errors = $settings_check
|
||||
let all_warnings = ($sops_config_check | append $sops_key_check)
|
||||
|
||||
{
|
||||
valid: (($all_errors | length) == 0),
|
||||
errors: $all_errors,
|
||||
warnings: $all_warnings
|
||||
}
|
||||
}
|
||||
|
||||
# Main validation function - runs all validation checks
|
||||
export def validate-config [
|
||||
config: record
|
||||
--detailed = false # Show detailed validation results
|
||||
--strict = false # Treat warnings as errors
|
||||
]: nothing -> record {
|
||||
# Run all validation checks
|
||||
let structure_result = (validate-config-structure $config)
|
||||
let paths_result = (validate-path-values $config)
|
||||
let types_result = (validate-data-types $config)
|
||||
let semantic_result = (validate-semantic-rules $config)
|
||||
let files_result = (validate-file-existence $config)
|
||||
|
||||
# Combine all results using immutable appending (Rule 3)
|
||||
let all_errors = (
|
||||
$structure_result.errors | append $paths_result.errors | append $types_result.errors |
|
||||
append $semantic_result.errors | append $files_result.errors
|
||||
)
|
||||
|
||||
let all_warnings = (
|
||||
$structure_result.warnings | append $paths_result.warnings | append $types_result.warnings |
|
||||
append $semantic_result.warnings | append $files_result.warnings
|
||||
)
|
||||
|
||||
let has_errors = ($all_errors | length) > 0
|
||||
let has_warnings = ($all_warnings | length) > 0
|
||||
|
||||
# In strict mode, treat warnings as errors
|
||||
let final_valid = if $strict {
|
||||
(not $has_errors) and (not $has_warnings)
|
||||
} else {
|
||||
not $has_errors
|
||||
}
|
||||
|
||||
# Throw error if validation fails and not in detailed mode
|
||||
if (not $detailed) and (not $final_valid) {
|
||||
let error_messages = ($all_errors | each { |err| $err.message })
|
||||
let warning_messages = if $strict { ($all_warnings | each { |warn| $warn.message }) } else { [] }
|
||||
let combined_messages = ($error_messages | append $warning_messages)
|
||||
|
||||
error make {
|
||||
msg: ($combined_messages | str join "; ")
|
||||
}
|
||||
}
|
||||
|
||||
# Return detailed results
|
||||
{
|
||||
valid: $final_valid,
|
||||
errors: $all_errors,
|
||||
warnings: $all_warnings,
|
||||
summary: {
|
||||
total_errors: ($all_errors | length),
|
||||
total_warnings: ($all_warnings | length),
|
||||
checks_run: 5,
|
||||
structure_valid: $structure_result.valid,
|
||||
paths_valid: $paths_result.valid,
|
||||
types_valid: $types_result.valid,
|
||||
semantic_valid: $semantic_result.valid,
|
||||
files_valid: $files_result.valid
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,367 +1,526 @@
|
||||
# CoreDNS Orchestrator Integration
|
||||
# Automatic DNS updates when infrastructure changes
|
||||
#!/usr/bin/env nu
|
||||
|
||||
use ../utils/log.nu *
|
||||
use ../config/loader.nu get-config
|
||||
use zones.nu [add-a-record remove-record]
|
||||
# Integration Functions for External Systems
|
||||
#
|
||||
# Provides integration with:
|
||||
# - MCP (Model Context Protocol) servers
|
||||
# - Rust installer binary
|
||||
# - REST APIs
|
||||
# - Webhook notifications
|
||||
|
||||
# Register server in DNS when created
|
||||
export def register-server-in-dns [
|
||||
hostname: string # Server hostname
|
||||
ip_address: string # Server IP address
|
||||
zone?: string = "provisioning.local" # DNS zone
|
||||
--check
|
||||
] -> bool {
|
||||
log info $"Registering server in DNS: ($hostname) -> ($ip_address)"
|
||||
# Load configuration from MCP server
|
||||
#
|
||||
# Queries the MCP server for deployment configuration using
|
||||
# the Model Context Protocol.
|
||||
#
|
||||
# @param mcp_url: MCP server URL
|
||||
# @returns: Deployment configuration record
|
||||
export def load-config-from-mcp [mcp_url: string]: nothing -> record {
|
||||
print $"📡 Loading configuration from MCP server: ($mcp_url)"
|
||||
|
||||
if $check {
|
||||
log info "Check mode: Would register server in DNS"
|
||||
return true
|
||||
}
|
||||
|
||||
# Check if dynamic DNS is enabled
|
||||
let config = get-config
|
||||
let coredns_config = $config.coredns? | default {}
|
||||
let dynamic_enabled = $coredns_config.dynamic_updates?.enabled? | default true
|
||||
|
||||
if not $dynamic_enabled {
|
||||
log warn "Dynamic DNS updates are disabled"
|
||||
return false
|
||||
}
|
||||
|
||||
# Add A record to zone
|
||||
let result = add-a-record $zone $hostname $ip_address --comment "Auto-registered server"
|
||||
|
||||
if $result {
|
||||
log info $"Server registered in DNS: ($hostname)"
|
||||
true
|
||||
} else {
|
||||
log error $"Failed to register server in DNS: ($hostname)"
|
||||
false
|
||||
# MCP request payload
|
||||
let request = {
|
||||
jsonrpc: "2.0"
|
||||
id: 1
|
||||
method: "config/get"
|
||||
params: {
|
||||
type: "deployment"
|
||||
include_defaults: true
|
||||
}
|
||||
}
|
||||
|
||||
# Unregister server from DNS when deleted
|
||||
export def unregister-server-from-dns [
|
||||
hostname: string # Server hostname
|
||||
zone?: string = "provisioning.local" # DNS zone
|
||||
--check
|
||||
] -> bool {
|
||||
log info $"Unregistering server from DNS: ($hostname)"
|
||||
try {
|
||||
let response = (
|
||||
http post $mcp_url --content-type "application/json" ($request | to json)
|
||||
)
|
||||
|
||||
if $check {
|
||||
log info "Check mode: Would unregister server from DNS"
|
||||
return true
|
||||
}
|
||||
|
||||
# Check if dynamic DNS is enabled
|
||||
let config = get-config
|
||||
let coredns_config = $config.coredns? | default {}
|
||||
let dynamic_enabled = $coredns_config.dynamic_updates?.enabled? | default true
|
||||
|
||||
if not $dynamic_enabled {
|
||||
log warn "Dynamic DNS updates are disabled"
|
||||
return false
|
||||
}
|
||||
|
||||
# Remove record from zone
|
||||
let result = remove-record $zone $hostname
|
||||
|
||||
if $result {
|
||||
log info $"Server unregistered from DNS: ($hostname)"
|
||||
true
|
||||
} else {
|
||||
log error $"Failed to unregister server from DNS: ($hostname)"
|
||||
false
|
||||
if "error" in ($response | columns) {
|
||||
error make {
|
||||
msg: $"MCP error: ($response.error.message)"
|
||||
label: {text: $"Code: ($response.error.code)"}
|
||||
}
|
||||
}
|
||||
|
||||
# Bulk register servers
|
||||
export def bulk-register-servers [
|
||||
servers: list # List of {hostname: str, ip: str}
|
||||
zone?: string = "provisioning.local"
|
||||
--check
|
||||
] -> record {
|
||||
log info $"Bulk registering ($servers | length) servers in DNS"
|
||||
if "result" not-in ($response | columns) {
|
||||
error make {msg: "Invalid MCP response: missing result"}
|
||||
}
|
||||
|
||||
if $check {
|
||||
return {
|
||||
total: ($servers | length)
|
||||
registered: ($servers | length)
|
||||
failed: 0
|
||||
check_mode: true
|
||||
print "✅ Configuration loaded from MCP server"
|
||||
$response.result
|
||||
|
||||
} catch {|err|
|
||||
error make {
|
||||
msg: $"Failed to load config from MCP: ($mcp_url)"
|
||||
label: {text: $err.msg}
|
||||
help: "Ensure MCP server is running and accessible"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mut registered = 0
|
||||
mut failed = 0
|
||||
# Load configuration from REST API
|
||||
#
|
||||
# Fetches deployment configuration from a REST API endpoint.
|
||||
#
|
||||
# @param api_url: API endpoint URL
|
||||
# @returns: Deployment configuration record
|
||||
export def load-config-from-api [api_url: string]: nothing -> record {
|
||||
print $"🌐 Loading configuration from API: ($api_url)"
|
||||
|
||||
for server in $servers {
|
||||
let hostname = $server.hostname
|
||||
let ip = $server.ip
|
||||
try {
|
||||
let response = (http get $api_url --max-time 30sec)
|
||||
|
||||
let result = register-server-in-dns $hostname $ip $zone
|
||||
if "config" not-in ($response | columns) {
|
||||
error make {msg: "Invalid API response: missing 'config' field"}
|
||||
}
|
||||
|
||||
if $result {
|
||||
$registered = $registered + 1
|
||||
} else {
|
||||
$failed = $failed + 1
|
||||
print "✅ Configuration loaded from API"
|
||||
$response.config
|
||||
|
||||
} catch {|err|
|
||||
error make {
|
||||
msg: $"Failed to load config from API: ($api_url)"
|
||||
label: {text: $err.msg}
|
||||
help: "Check API endpoint and network connectivity"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Send notification to webhook
|
||||
#
|
||||
# Sends deployment event notifications to a configured webhook URL.
|
||||
# Useful for integration with Slack, Discord, Microsoft Teams, etc.
|
||||
#
|
||||
# @param webhook_url: Webhook URL
|
||||
# @param payload: Notification payload record
|
||||
# @returns: Nothing
|
||||
export def notify-webhook [webhook_url: string, payload: record]: nothing -> nothing {
|
||||
try {
|
||||
http post $webhook_url --content-type "application/json" ($payload | to json)
|
||||
|
||||
null
|
||||
} catch {|err|
|
||||
# Don't fail deployment on webhook errors, just log
|
||||
print $"⚠️ Warning: Failed to send webhook notification: ($err.msg)"
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
# Call Rust installer binary with arguments
|
||||
#
|
||||
# Invokes the Rust installer binary with specified arguments,
|
||||
# capturing output and exit code.
|
||||
#
|
||||
# @param args: List of arguments to pass to installer
|
||||
# @returns: Installer execution result record
|
||||
export def call-installer [args: list<string>]: nothing -> record {
|
||||
let installer_path = get-installer-path
|
||||
|
||||
print $"🚀 Calling installer: ($installer_path) ($args | str join ' ')"
|
||||
|
||||
try {
|
||||
let output = (^$installer_path ...$args | complete)
|
||||
|
||||
{
|
||||
total: ($servers | length)
|
||||
registered: $registered
|
||||
failed: $failed
|
||||
success: ($output.exit_code == 0)
|
||||
exit_code: $output.exit_code
|
||||
stdout: $output.stdout
|
||||
stderr: $output.stderr
|
||||
timestamp: (date now)
|
||||
}
|
||||
} catch {|err|
|
||||
{
|
||||
success: false
|
||||
exit_code: -1
|
||||
error: $err.msg
|
||||
timestamp: (date now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Bulk unregister servers
|
||||
export def bulk-unregister-servers [
|
||||
hostnames: list<string> # List of hostnames
|
||||
zone?: string = "provisioning.local"
|
||||
--check
|
||||
] -> record {
|
||||
log info $"Bulk unregistering ($hostnames | length) servers from DNS"
|
||||
# Run installer in headless mode with config file
|
||||
#
|
||||
# Executes the Rust installer in headless mode using a
|
||||
# configuration file.
|
||||
#
|
||||
# @param config_path: Path to configuration file
|
||||
# @param auto_confirm: Auto-confirm prompts
|
||||
# @returns: Installer execution result record
|
||||
export def run-installer-headless [
|
||||
config_path: path
|
||||
--auto-confirm
|
||||
]: nothing -> record {
|
||||
mut args = ["--headless", "--config", $config_path]
|
||||
|
||||
if $check {
|
||||
return {
|
||||
total: ($hostnames | length)
|
||||
unregistered: ($hostnames | length)
|
||||
failed: 0
|
||||
check_mode: true
|
||||
}
|
||||
if $auto_confirm {
|
||||
$args = ($args | append "--yes")
|
||||
}
|
||||
|
||||
mut unregistered = 0
|
||||
mut failed = 0
|
||||
|
||||
for hostname in $hostnames {
|
||||
let result = unregister-server-from-dns $hostname $zone
|
||||
|
||||
if $result {
|
||||
$unregistered = $unregistered + 1
|
||||
} else {
|
||||
$failed = $failed + 1
|
||||
}
|
||||
call-installer $args
|
||||
}
|
||||
|
||||
# Run installer interactively
|
||||
#
|
||||
# Launches the Rust installer in interactive TUI mode.
|
||||
#
|
||||
# @returns: Installer execution result record
|
||||
export def run-installer-interactive []: nothing -> record {
|
||||
let installer_path = get-installer-path
|
||||
|
||||
print $"🚀 Launching interactive installer: ($installer_path)"
|
||||
|
||||
try {
|
||||
# Run without capturing output (interactive mode)
|
||||
^$installer_path
|
||||
|
||||
{
|
||||
total: ($hostnames | length)
|
||||
unregistered: $unregistered
|
||||
failed: $failed
|
||||
success: true
|
||||
mode: "interactive"
|
||||
message: "Interactive installer completed"
|
||||
timestamp: (date now)
|
||||
}
|
||||
} catch {|err|
|
||||
{
|
||||
success: false
|
||||
mode: "interactive"
|
||||
error: $err.msg
|
||||
timestamp: (date now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Sync DNS with infrastructure state
|
||||
export def sync-dns-with-infra [
|
||||
infrastructure: string # Infrastructure name
|
||||
--zone: string = "provisioning.local"
|
||||
--check
|
||||
] -> record {
|
||||
log info $"Syncing DNS with infrastructure: ($infrastructure)"
|
||||
# Pass deployment config to installer via CLI args
|
||||
#
|
||||
# Converts a deployment configuration record into CLI arguments
|
||||
# for the Rust installer binary.
|
||||
#
|
||||
# @param config: Deployment configuration record
|
||||
# @returns: List of CLI arguments
|
||||
export def config-to-cli-args [config: record]: nothing -> list<string> {
|
||||
mut args = ["--headless"]
|
||||
|
||||
if $check {
|
||||
log info "Check mode: Would sync DNS with infrastructure"
|
||||
return {
|
||||
synced: true
|
||||
check_mode: true
|
||||
# Add platform
|
||||
$args = ($args | append ["--platform", $config.platform])
|
||||
|
||||
# Add mode
|
||||
$args = ($args | append ["--mode", $config.mode])
|
||||
|
||||
# Add domain
|
||||
$args = ($args | append ["--domain", $config.domain])
|
||||
|
||||
# Add services (comma-separated)
|
||||
let services = $config.services
|
||||
| where enabled
|
||||
| get name
|
||||
| str join ","
|
||||
|
||||
if $services != "" {
|
||||
$args = ($args | append ["--services", $services])
|
||||
}
|
||||
|
||||
$args
|
||||
}
|
||||
|
||||
# Deploy using installer binary
|
||||
#
|
||||
# High-level function to deploy using the Rust installer binary
|
||||
# with the given configuration.
|
||||
#
|
||||
# @param config: Deployment configuration record
|
||||
# @param auto_confirm: Auto-confirm prompts
|
||||
# @returns: Deployment result record
|
||||
export def deploy-with-installer [
|
||||
config: record
|
||||
--auto-confirm
|
||||
]: nothing -> record {
|
||||
print "🚀 Deploying using Rust installer binary..."
|
||||
|
||||
# Convert config to CLI args
|
||||
mut args = (config-to-cli-args $config)
|
||||
|
||||
if $auto_confirm {
|
||||
$args = ($args | append "--yes")
|
||||
}
|
||||
|
||||
# Execute installer
|
||||
let result = call-installer $args
|
||||
|
||||
if $result.success {
|
||||
print "✅ Installer deployment successful"
|
||||
{
|
||||
success: true
|
||||
method: "installer_binary"
|
||||
config: $config
|
||||
timestamp: (date now)
|
||||
}
|
||||
} else {
|
||||
print $"❌ Installer deployment failed: ($result.stderr)"
|
||||
{
|
||||
success: false
|
||||
method: "installer_binary"
|
||||
error: $result.stderr
|
||||
exit_code: $result.exit_code
|
||||
timestamp: (date now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get infrastructure state from config
|
||||
let config = get-config
|
||||
let workspace_path = get-workspace-path
|
||||
|
||||
# Load infrastructure servers
|
||||
let infra_path = $"($workspace_path)/infra/($infrastructure)"
|
||||
|
||||
if not ($infra_path | path exists) {
|
||||
log error $"Infrastructure not found: ($infrastructure)"
|
||||
return {
|
||||
synced: false
|
||||
error: "Infrastructure not found"
|
||||
# Query MCP server for deployment status
|
||||
#
|
||||
# Retrieves deployment status information from MCP server.
|
||||
#
|
||||
# @param mcp_url: MCP server URL
|
||||
# @param deployment_id: Deployment identifier
|
||||
# @returns: Deployment status record
|
||||
export def query-mcp-status [mcp_url: string, deployment_id: string]: nothing -> record {
|
||||
let request = {
|
||||
jsonrpc: "2.0"
|
||||
id: 1
|
||||
method: "deployment/status"
|
||||
params: {
|
||||
deployment_id: $deployment_id
|
||||
}
|
||||
}
|
||||
|
||||
# Get server list from infrastructure
|
||||
let servers = get-infra-servers $infrastructure
|
||||
try {
|
||||
let response = (
|
||||
http post $mcp_url --content-type "application/json" ($request | to json)
|
||||
)
|
||||
|
||||
if ($servers | is-empty) {
|
||||
log warn $"No servers found in infrastructure: ($infrastructure)"
|
||||
return {
|
||||
synced: true
|
||||
servers_synced: 0
|
||||
if "error" in ($response | columns) {
|
||||
error make {
|
||||
msg: $"MCP error: ($response.error.message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Register all servers
|
||||
let result = bulk-register-servers $servers $zone
|
||||
$response.result
|
||||
|
||||
} catch {|err|
|
||||
error make {
|
||||
msg: $"Failed to query MCP status: ($err.msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Register deployment with API
|
||||
#
|
||||
# Registers a new deployment with the external API and returns
|
||||
# a deployment ID for tracking.
|
||||
#
|
||||
# @param api_url: API endpoint URL
|
||||
# @param config: Deployment configuration
|
||||
# @returns: Registration result with deployment ID
|
||||
export def register-deployment-with-api [api_url: string, config: record]: nothing -> record {
|
||||
let payload = {
|
||||
platform: $config.platform
|
||||
mode: $config.mode
|
||||
domain: $config.domain
|
||||
services: ($config.services | get name)
|
||||
started_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
}
|
||||
|
||||
try {
|
||||
let response = (
|
||||
http post $api_url --content-type "application/json" ($payload | to json)
|
||||
)
|
||||
|
||||
if "deployment_id" not-in ($response | columns) {
|
||||
error make {msg: "API did not return deployment_id"}
|
||||
}
|
||||
|
||||
print $"✅ Deployment registered with API: ($response.deployment_id)"
|
||||
|
||||
{
|
||||
synced: true
|
||||
servers_synced: $result.registered
|
||||
servers_failed: $result.failed
|
||||
success: true
|
||||
deployment_id: $response.deployment_id
|
||||
api_url: $api_url
|
||||
}
|
||||
|
||||
} catch {|err|
|
||||
print $"⚠️ Warning: Failed to register with API: ($err.msg)"
|
||||
{
|
||||
success: false
|
||||
error: $err.msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get infrastructure servers
|
||||
def get-infra-servers [
|
||||
infrastructure: string
|
||||
] -> list {
|
||||
# This would normally load from infrastructure state/config
|
||||
# For now, return empty list as placeholder
|
||||
log debug $"Loading servers from infrastructure: ($infrastructure)"
|
||||
# Update deployment status via API
|
||||
#
|
||||
# Updates deployment status on external API for tracking and monitoring.
|
||||
#
|
||||
# @param api_url: API endpoint URL
|
||||
# @param deployment_id: Deployment identifier
|
||||
# @param status: Status update record
|
||||
# @returns: Update result record
|
||||
export def update-deployment-status [
|
||||
api_url: string
|
||||
deployment_id: string
|
||||
status: record
|
||||
]: nothing -> record {
|
||||
let update_url = $"($api_url)/($deployment_id)/status"
|
||||
|
||||
# TODO: Implement proper infrastructure server loading
|
||||
# Should read from:
|
||||
# - workspace/infra/{name}/servers.yaml
|
||||
# - workspace/runtime/state/{name}/servers.json
|
||||
# - Provider-specific state files
|
||||
try {
|
||||
http patch $update_url --content-type "application/json" ($status | to json)
|
||||
|
||||
[]
|
||||
{success: true}
|
||||
|
||||
} catch {|err|
|
||||
print $"⚠️ Warning: Failed to update deployment status: ($err.msg)"
|
||||
{success: false, error: $err.msg}
|
||||
}
|
||||
}
|
||||
|
||||
# Get workspace path
|
||||
def get-workspace-path [] -> string {
|
||||
let config = get-config
|
||||
let workspace = $config.workspace?.path? | default "workspace_librecloud"
|
||||
|
||||
$workspace | path expand
|
||||
# Send Slack notification
|
||||
#
|
||||
# Sends formatted notification to Slack webhook.
|
||||
#
|
||||
# @param webhook_url: Slack webhook URL
|
||||
# @param message: Message text
|
||||
# @param color: Message color (good, warning, danger)
|
||||
# @returns: Nothing
|
||||
export def notify-slack [
|
||||
webhook_url: string
|
||||
message: string
|
||||
--color: string = "good"
|
||||
]: nothing -> nothing {
|
||||
let payload = {
|
||||
attachments: [{
|
||||
color: $color
|
||||
text: $message
|
||||
footer: "Provisioning Platform Installer"
|
||||
ts: (date now | format date "%s")
|
||||
}]
|
||||
}
|
||||
|
||||
# Check if DNS integration is enabled
|
||||
export def is-dns-integration-enabled [] -> bool {
|
||||
let config = get-config
|
||||
let coredns_config = $config.coredns? | default {}
|
||||
|
||||
let mode = $coredns_config.mode? | default "disabled"
|
||||
let dynamic_enabled = $coredns_config.dynamic_updates?.enabled? | default false
|
||||
|
||||
($mode != "disabled") and $dynamic_enabled
|
||||
notify-webhook $webhook_url $payload
|
||||
}
|
||||
|
||||
# Register service in DNS
|
||||
export def register-service-in-dns [
|
||||
service_name: string # Service name
|
||||
hostname: string # Hostname or IP
|
||||
port?: int # Port number (for SRV record)
|
||||
zone?: string = "provisioning.local"
|
||||
--check
|
||||
] -> bool {
|
||||
log info $"Registering service in DNS: ($service_name) -> ($hostname)"
|
||||
# Send Discord notification
|
||||
#
|
||||
# Sends formatted notification to Discord webhook.
|
||||
#
|
||||
# @param webhook_url: Discord webhook URL
|
||||
# @param message: Message text
|
||||
# @param success: Whether deployment was successful
|
||||
# @returns: Nothing
|
||||
export def notify-discord [
|
||||
webhook_url: string
|
||||
message: string
|
||||
--success
|
||||
]: nothing -> nothing {
|
||||
let color = if $success { 3066993 } else { 15158332 } # Green or Red
|
||||
let emoji = if $success { "✅" } else { "❌" }
|
||||
|
||||
if $check {
|
||||
log info "Check mode: Would register service in DNS"
|
||||
return true
|
||||
let payload = {
|
||||
embeds: [{
|
||||
title: $"($emoji) Provisioning Platform Deployment"
|
||||
description: $message
|
||||
color: $color
|
||||
timestamp: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
footer: {
|
||||
text: "Provisioning Platform Installer"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
# Add CNAME or A record for service
|
||||
let result = add-a-record $zone $service_name $hostname --comment $"Service: ($service_name)"
|
||||
notify-webhook $webhook_url $payload
|
||||
}
|
||||
|
||||
if $result {
|
||||
log info $"Service registered in DNS: ($service_name)"
|
||||
true
|
||||
# Send Microsoft Teams notification
|
||||
#
|
||||
# Sends formatted notification to Microsoft Teams webhook.
|
||||
#
|
||||
# @param webhook_url: Teams webhook URL
|
||||
# @param title: Notification title
|
||||
# @param message: Message text
|
||||
# @param success: Whether deployment was successful
|
||||
# @returns: Nothing
|
||||
export def notify-teams [
|
||||
webhook_url: string
|
||||
title: string
|
||||
message: string
|
||||
--success
|
||||
]: nothing -> nothing {
|
||||
let theme_color = if $success { "00FF00" } else { "FF0000" }
|
||||
|
||||
let payload = {
|
||||
"@type": "MessageCard"
|
||||
"@context": "https://schema.org/extensions"
|
||||
summary: $title
|
||||
themeColor: $theme_color
|
||||
title: $title
|
||||
text: $message
|
||||
}
|
||||
|
||||
notify-webhook $webhook_url $payload
|
||||
}
|
||||
|
||||
# Execute MCP tool call
|
||||
#
|
||||
# Executes a tool/function call via MCP server.
|
||||
#
|
||||
# @param mcp_url: MCP server URL
|
||||
# @param tool_name: Name of tool to execute
|
||||
# @param arguments: Tool arguments record
|
||||
# @returns: Tool execution result
|
||||
export def execute-mcp-tool [
|
||||
mcp_url: string
|
||||
tool_name: string
|
||||
arguments: record
|
||||
]: nothing -> record {
|
||||
let request = {
|
||||
jsonrpc: "2.0"
|
||||
id: 1
|
||||
method: "tools/call"
|
||||
params: {
|
||||
name: $tool_name
|
||||
arguments: $arguments
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let response = (
|
||||
http post $mcp_url --content-type "application/json" ($request | to json)
|
||||
)
|
||||
|
||||
if "error" in ($response | columns) {
|
||||
error make {
|
||||
msg: $"MCP tool execution error: ($response.error.message)"
|
||||
}
|
||||
}
|
||||
|
||||
$response.result
|
||||
|
||||
} catch {|err|
|
||||
error make {
|
||||
msg: $"Failed to execute MCP tool: ($err.msg)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get installer binary path (helper function)
|
||||
#
|
||||
# @returns: Path to installer binary
|
||||
def get-installer-path []: nothing -> path {
|
||||
let installer_dir = $env.PWD | path dirname
|
||||
let installer_name = if $nu.os-info.name == "windows" {
|
||||
"provisioning-installer.exe"
|
||||
} else {
|
||||
log error $"Failed to register service in DNS: ($service_name)"
|
||||
false
|
||||
}
|
||||
"provisioning-installer"
|
||||
}
|
||||
|
||||
# Unregister service from DNS
|
||||
export def unregister-service-from-dns [
|
||||
service_name: string # Service name
|
||||
zone?: string = "provisioning.local"
|
||||
--check
|
||||
] -> bool {
|
||||
log info $"Unregistering service from DNS: ($service_name)"
|
||||
# Check target/release first, then target/debug
|
||||
let release_path = $installer_dir | path join "target" "release" $installer_name
|
||||
let debug_path = $installer_dir | path join "target" "debug" $installer_name
|
||||
|
||||
if $check {
|
||||
log info "Check mode: Would unregister service from DNS"
|
||||
return true
|
||||
}
|
||||
|
||||
let result = remove-record $zone $service_name
|
||||
|
||||
if $result {
|
||||
log info $"Service unregistered from DNS: ($service_name)"
|
||||
true
|
||||
if ($release_path | path exists) {
|
||||
$release_path
|
||||
} else if ($debug_path | path exists) {
|
||||
$debug_path
|
||||
} else {
|
||||
log error $"Failed to unregister service from DNS: ($service_name)"
|
||||
false
|
||||
error make {
|
||||
msg: "Installer binary not found"
|
||||
help: "Build with: cargo build --release"
|
||||
}
|
||||
}
|
||||
|
||||
# Hook: After server creation
|
||||
export def "dns-hook after-server-create" [
|
||||
server: record # Server record with hostname and ip
|
||||
--check
|
||||
] -> bool {
|
||||
let hostname = $server.hostname
|
||||
let ip = $server.ip_address? | default ($server.ip? | default "")
|
||||
|
||||
if ($ip | is-empty) {
|
||||
log warn $"Server ($hostname) has no IP address, skipping DNS registration"
|
||||
return false
|
||||
}
|
||||
|
||||
# Check if auto-register is enabled
|
||||
let config = get-config
|
||||
let coredns_config = $config.coredns? | default {}
|
||||
let auto_register = $coredns_config.dynamic_updates?.auto_register_servers? | default true
|
||||
|
||||
if not $auto_register {
|
||||
log debug "Auto-register servers is disabled"
|
||||
return false
|
||||
}
|
||||
|
||||
register-server-in-dns $hostname $ip --check=$check
|
||||
}
|
||||
|
||||
# Hook: Before server deletion
|
||||
export def "dns-hook before-server-delete" [
|
||||
server: record # Server record with hostname
|
||||
--check
|
||||
] -> bool {
|
||||
let hostname = $server.hostname
|
||||
|
||||
# Check if auto-unregister is enabled
|
||||
let config = get-config
|
||||
let coredns_config = $config.coredns? | default {}
|
||||
let auto_unregister = $coredns_config.dynamic_updates?.auto_unregister_servers? | default true
|
||||
|
||||
if not $auto_unregister {
|
||||
log debug "Auto-unregister servers is disabled"
|
||||
return false
|
||||
}
|
||||
|
||||
unregister-server-from-dns $hostname --check=$check
|
||||
}
|
||||
|
||||
# Hook: After cluster creation
|
||||
export def "dns-hook after-cluster-create" [
|
||||
cluster: record # Cluster record
|
||||
--check
|
||||
] -> bool {
|
||||
let cluster_name = $cluster.name
|
||||
let master_ip = $cluster.master_ip? | default ""
|
||||
|
||||
if ($master_ip | is-empty) {
|
||||
log warn $"Cluster ($cluster_name) has no master IP, skipping DNS registration"
|
||||
return false
|
||||
}
|
||||
|
||||
# Register cluster master
|
||||
register-service-in-dns $"($cluster_name)-master" $master_ip --check=$check
|
||||
}
|
||||
|
||||
# Hook: Before cluster deletion
|
||||
export def "dns-hook before-cluster-delete" [
|
||||
cluster: record # Cluster record
|
||||
--check
|
||||
] -> bool {
|
||||
let cluster_name = $cluster.name
|
||||
|
||||
# Unregister cluster master
|
||||
unregister-service-from-dns $"($cluster_name)-master" --check=$check
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
# myscript.nu
|
||||
export def about_info [
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let info = if ( $env.CURRENT_FILE? | into string ) != "" { (^grep "^# Info:" $env.CURRENT_FILE ) | str replace "# Info: " "" } else { "" }
|
||||
$"
|
||||
USAGE provisioning -k cloud-path file-settings.yaml provider-options
|
||||
|
||||
@ -4,7 +4,7 @@ use ../utils/on_select.nu run_on_selection
|
||||
export def get_provisioning_info [
|
||||
dir_path: string
|
||||
target: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# task root path target will be empty
|
||||
let item = if $target != "" { $target } else { ($dir_path | path basename) }
|
||||
let full_path = if $target != "" { $"($dir_path)/($item)" } else { $dir_path }
|
||||
@ -42,7 +42,7 @@ export def get_provisioning_info [
|
||||
}
|
||||
export def providers_list [
|
||||
mode?: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let configured_path = (get-providers-path)
|
||||
let providers_path = if ($configured_path | is-empty) {
|
||||
# Fallback to system providers directory
|
||||
@ -72,7 +72,7 @@ export def providers_list [
|
||||
}
|
||||
}
|
||||
}
|
||||
def detect_infra_context []: nothing -> string {
|
||||
def detect_infra_context [] {
|
||||
# Detect if we're inside an infrastructure directory OR using --infra flag
|
||||
# Priority: 1) PROVISIONING_INFRA env var (from --infra flag), 2) pwd path detection
|
||||
|
||||
@ -119,7 +119,7 @@ def detect_infra_context []: nothing -> string {
|
||||
$first_component
|
||||
}
|
||||
|
||||
def get_infra_taskservs [infra_name: string]: nothing -> list {
|
||||
def get_infra_taskservs [infra_name: string] {
|
||||
# Get taskservs from specific infrastructure directory
|
||||
let current_path = pwd
|
||||
|
||||
@ -195,7 +195,7 @@ def get_infra_taskservs [infra_name: string]: nothing -> list {
|
||||
}
|
||||
|
||||
export def taskservs_list [
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Detect if we're inside an infrastructure directory
|
||||
let infra_context = detect_infra_context
|
||||
|
||||
@ -222,7 +222,7 @@ export def taskservs_list [
|
||||
} | flatten
|
||||
}
|
||||
export def cluster_list [
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Determine workspace base path
|
||||
# Try: 1) check if we're already in workspace, 2) look for workspace_librecloud relative to pwd
|
||||
let current_path = pwd
|
||||
@ -252,7 +252,7 @@ export def cluster_list [
|
||||
} | flatten | default []
|
||||
}
|
||||
export def infras_list [
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Determine workspace base path
|
||||
# Try: 1) check if we're already in workspace, 2) look for workspace_librecloud relative to pwd
|
||||
let current_path = pwd
|
||||
@ -287,7 +287,7 @@ export def on_list [
|
||||
target_list: string
|
||||
cmd: string
|
||||
ops: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
#use utils/on_select.nu run_on_selection
|
||||
match $target_list {
|
||||
"providers" | "p" => {
|
||||
|
||||
@ -1,165 +1,558 @@
|
||||
use std
|
||||
use utils select_file_list
|
||||
use config/accessor.nu *
|
||||
#!/usr/bin/env nu
|
||||
|
||||
export def deploy_remove [
|
||||
settings: record
|
||||
str_match?: string
|
||||
]: nothing -> nothing {
|
||||
let match = if $str_match != "" { $str_match |str trim } else { (date now | format date (get-match-date)) }
|
||||
let str_out_path = ($settings.data.runset.output_path | default "" | str replace "~" $env.HOME | str replace "NOW" $match)
|
||||
let prov_local_bin_path = ($settings.data.prov_local_bin_path | default "" | str replace "~" $env.HOME )
|
||||
if $prov_local_bin_path != "" and ($prov_local_bin_path | path join "on_deploy_remove" | path exists ) {
|
||||
^($prov_local_bin_path | path join "on_deploy_remove")
|
||||
# Multi-Region HA Workspace Deployment Script
|
||||
# Orchestrates deployment across US East (DigitalOcean), EU Central (Hetzner), Asia Pacific (AWS)
|
||||
# Features: Regional health checks, VPN tunnels, global DNS, failover configuration
|
||||
|
||||
def main [--debug: bool = false, --region: string = "all"] {
|
||||
print "🌍 Multi-Region High Availability Deployment"
|
||||
print "──────────────────────────────────────────────────"
|
||||
|
||||
if $debug {
|
||||
print "✓ Debug mode enabled"
|
||||
}
|
||||
let out_path = if ($str_out_path | str starts-with "/") { $str_out_path
|
||||
} else { ($settings.infra_path | path join $settings.infra | path join $str_out_path) }
|
||||
|
||||
if $out_path == "" or not ($out_path | path dirname | path exists ) { return }
|
||||
mut last_provider = ""
|
||||
for server in $settings.data.servers {
|
||||
let provider = $server.provider | default ""
|
||||
if $provider == $last_provider {
|
||||
continue
|
||||
# Determine which regions to deploy
|
||||
let regions = if $region == "all" {
|
||||
["us-east", "eu-central", "asia-southeast"]
|
||||
} else {
|
||||
$last_provider = $provider
|
||||
[$region]
|
||||
}
|
||||
if (".git" | path exists) or (".." | path join ".git" | path exists) {
|
||||
^git rm -rf ($out_path | path dirname | path join $"($provider)_cmd.*") | ignore
|
||||
|
||||
print $"\n📋 Deploying to regions: ($regions | str join ', ')"
|
||||
|
||||
# Step 1: Validate configuration
|
||||
print "\n📋 Step 1: Validating configuration..."
|
||||
validate_environment
|
||||
|
||||
# Step 2: Deploy US East (Primary)
|
||||
if ("us-east" in $regions) {
|
||||
print "\n☁️ Step 2a: Deploying US East (DigitalOcean - Primary)..."
|
||||
deploy_us_east_digitalocean
|
||||
}
|
||||
let res = (^rm -rf ...(glob ($out_path | path dirname | path join $"($provider)_cmd.*")) | complete)
|
||||
if $res.exit_code == 0 {
|
||||
print $"(_ansi purple_bold)Deploy files(_ansi reset) ($out_path | path dirname | path join $"($provider)_cmd.*") (_ansi red)removed(_ansi reset)"
|
||||
|
||||
# Step 3: Deploy EU Central (Secondary)
|
||||
if ("eu-central" in $regions) {
|
||||
print "\n☁️ Step 2b: Deploying EU Central (Hetzner - Secondary)..."
|
||||
deploy_eu_central_hetzner
|
||||
}
|
||||
|
||||
# Step 4: Deploy Asia Pacific (Tertiary)
|
||||
if ("asia-southeast" in $regions) {
|
||||
print "\n☁️ Step 2c: Deploying Asia Pacific (AWS - Tertiary)..."
|
||||
deploy_asia_pacific_aws
|
||||
}
|
||||
if (".git" | path exists) or (".." | path join ".git" | path exists) {
|
||||
^git rm -rf ...(glob ($out_path | path dirname | path join $"($match)_*")) | ignore
|
||||
|
||||
# Step 5: Setup VPN tunnels (only if deploying multiple regions)
|
||||
if (($regions | length) > 1) {
|
||||
print "\n🔐 Step 3: Setting up VPN tunnels for inter-region communication..."
|
||||
setup_vpn_tunnels
|
||||
}
|
||||
let result = (^rm -rf ...(glob ($out_path | path dirname | path join $"($match)_*")) | complete)
|
||||
if $result.exit_code == 0 {
|
||||
print $"(_ansi purple_bold)Deploy files(_ansi reset) ($out_path | path dirname | path join $"($match)_*") (_ansi red)removed(_ansi reset)"
|
||||
|
||||
# Step 6: Configure global DNS
|
||||
if (($regions | length) == 3) {
|
||||
print "\n🌐 Step 4: Configuring global DNS and failover policies..."
|
||||
setup_global_dns
|
||||
}
|
||||
|
||||
# Step 7: Configure database replication
|
||||
if (($regions | length) > 1) {
|
||||
print "\n🗄️ Step 5: Configuring database replication..."
|
||||
setup_database_replication
|
||||
}
|
||||
|
||||
# Step 8: Verify deployment
|
||||
print "\n✅ Step 6: Verifying deployment across regions..."
|
||||
verify_multi_region_deployment
|
||||
|
||||
print "\n🎉 Multi-region HA deployment complete!"
|
||||
print "✓ Application is now live across 3 geographic regions with automatic failover"
|
||||
print ""
|
||||
print "Next steps:"
|
||||
print "1. Configure SSL/TLS certificates for all regional endpoints"
|
||||
print "2. Deploy application to web servers in each region"
|
||||
print "3. Test failover by stopping a region and verifying automatic failover"
|
||||
print "4. Monitor replication lag and regional health status"
|
||||
}
|
||||
|
||||
def validate_environment [] {
|
||||
# Check required environment variables
|
||||
let required = [
|
||||
"DIGITALOCEAN_TOKEN",
|
||||
"HCLOUD_TOKEN",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY"
|
||||
]
|
||||
|
||||
print " Checking required environment variables..."
|
||||
$required | each {|var|
|
||||
if ($env | has $var) {
|
||||
print $" ✓ ($var) is set"
|
||||
} else {
|
||||
print $" ✗ ($var) is not set"
|
||||
error make {msg: $"Missing required environment variable: ($var)"}
|
||||
}
|
||||
}
|
||||
|
||||
export def on_item_for_cli [
|
||||
item: string
|
||||
item_name: string
|
||||
task: string
|
||||
task_name: string
|
||||
task_cmd: string
|
||||
show_msg: bool
|
||||
show_sel: bool
|
||||
]: nothing -> nothing {
|
||||
if $show_sel { print $"\n($item)" }
|
||||
let full_cmd = if ($task_cmd | str starts-with "ls ") { $'nu -c "($task_cmd) ($item)" ' } else { $'($task_cmd) ($item)'}
|
||||
if ($task_name | is-not-empty) {
|
||||
print $"($task_name) ($task_cmd) (_ansi purple_bold)($item_name)(_ansi reset) by paste in command line"
|
||||
}
|
||||
show_clip_to $full_cmd $show_msg
|
||||
}
|
||||
export def deploy_list [
|
||||
settings: record
|
||||
str_match: string
|
||||
onsel: string
|
||||
]: nothing -> nothing {
|
||||
let match = if $str_match != "" { $str_match |str trim } else { (date now | format date (get-match-date)) }
|
||||
let str_out_path = ($settings.data.runset.output_path | default "" | str replace "~" $env.HOME | str replace "NOW" $match)
|
||||
let prov_local_bin_path = ($settings.data.prov_local_bin_path | default "" | str replace "~" $env.HOME )
|
||||
let out_path = if ($str_out_path | str starts-with "/") { $str_out_path
|
||||
} else { ($settings.infra_path | path join $settings.infra | path join $str_out_path) }
|
||||
if $out_path == "" or not ($out_path | path dirname | path exists ) { return }
|
||||
let selection = match $onsel {
|
||||
"edit" | "editor" | "ed" | "e" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"view"| "vw" | "v" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"list"| "ls" | "l" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"tree"| "tr" | "t" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"code"| "c" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"shell"| "s" | "sh" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
"nu"| "n" => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
},
|
||||
_ => {
|
||||
select_file_list ($out_path | path dirname | path join $"($match)*") "Deploy files" true -1
|
||||
# Verify CLI tools
|
||||
let tools = ["doctl", "hcloud", "aws", "nickel"]
|
||||
print " Verifying CLI tools..."
|
||||
$tools | each {|tool|
|
||||
if (which $tool | is-not-empty) {
|
||||
print $" ✓ ($tool) is installed"
|
||||
} else {
|
||||
print $" ✗ ($tool) is not installed"
|
||||
error make {msg: $"Missing required tool: ($tool)"}
|
||||
}
|
||||
}
|
||||
if ($selection | is-not-empty ) {
|
||||
match $onsel {
|
||||
"edit" | "editor" | "ed" | "e" => {
|
||||
let cmd = ($env | get EDITOR? | default "vi")
|
||||
run-external $cmd $selection.name
|
||||
on_item_for_cli $selection.name ($selection.name | path basename) "edit" "Edit" $cmd false true
|
||||
},
|
||||
"view"| "vw" | "v" => {
|
||||
let cmd = if (^bash -c "type -P bat" | is-not-empty) { "bat" } else { "cat" }
|
||||
run-external $cmd $selection.name
|
||||
on_item_for_cli $selection.name ($selection.name | path basename) "view" "View" $cmd false true
|
||||
},
|
||||
"list"| "ls" | "l" => {
|
||||
let cmd = if (^bash -c "type -P nu" | is-not-empty) { "ls -s" } else { "ls -l" }
|
||||
let file_path = if $selection.type == "file" {
|
||||
($selection.name | path dirname)
|
||||
} else { $selection.name}
|
||||
run-external nu "-c" $"($cmd) ($file_path)"
|
||||
on_item_for_cli $file_path ($file_path | path basename) "list" "List" $cmd false false
|
||||
},
|
||||
"tree"| "tr" | "t" => {
|
||||
let cmd = if (^bash -c "type -P tree" | is-not-empty) { "tree -L 3" } else { "ls -s" }
|
||||
let file_path = if $selection.type == "file" {
|
||||
$selection.name | path dirname
|
||||
} else { $selection.name}
|
||||
run-external nu "-c" $"($cmd) ($file_path)"
|
||||
on_item_for_cli $file_path ($file_path | path basename) "tree" "Tree" $cmd false false
|
||||
},
|
||||
"code"| "c" => {
|
||||
let file_path = if $selection.type == "file" {
|
||||
$selection.name | path dirname
|
||||
} else { $selection.name}
|
||||
let cmd = $"code ($file_path)"
|
||||
run-external code $file_path
|
||||
show_titles
|
||||
print "Command "
|
||||
on_item_for_cli $file_path ($file_path | path basename) "tree" "Tree" $cmd false false
|
||||
},
|
||||
"shell" | "sh" | "s" => {
|
||||
let file_path = if $selection.type == "file" {
|
||||
$selection.name | path dirname
|
||||
} else { $selection.name}
|
||||
let cmd = $"bash -c " + $"cd ($file_path) ; ($env.SHELL)"
|
||||
print $"(_ansi default_dimmed)Use [ctrl-d] or 'exit' to end with(_ansi reset) ($env.SHELL)"
|
||||
run-external bash "-c" $"cd ($file_path) ; ($env.SHELL)"
|
||||
show_titles
|
||||
print "Command "
|
||||
on_item_for_cli $file_path ($file_path | path basename) "shell" "shell" $cmd false false
|
||||
},
|
||||
"nu"| "n" => {
|
||||
let file_path = if $selection.type == "file" {
|
||||
$selection.name | path dirname
|
||||
} else { $selection.name}
|
||||
let cmd = $"($env.NU) -i -e " + $"cd ($file_path)"
|
||||
print $"(_ansi default_dimmed)Use [ctrl-d] or 'exit' to end with(_ansi reset) nushell\n"
|
||||
run-external nu "-i" "-e" $"cd ($file_path)"
|
||||
on_item_for_cli $file_path ($file_path | path basename) "nu" "nushell" $cmd false false
|
||||
},
|
||||
_ => {
|
||||
on_item_for_cli $selection.name ($selection.name | path basename) "" "" "" false false
|
||||
print $selection
|
||||
|
||||
# Validate Nickel configuration
|
||||
print " Validating Nickel configuration..."
|
||||
try {
|
||||
nickel export workspace.ncl | from json | null
|
||||
print " ✓ Nickel configuration is valid"
|
||||
} catch {|err|
|
||||
error make {msg: $"Nickel validation failed: ($err)"}
|
||||
}
|
||||
|
||||
# Validate config.toml
|
||||
print " Validating config.toml..."
|
||||
try {
|
||||
let config = (open config.toml)
|
||||
print " ✓ config.toml is valid"
|
||||
} catch {|err|
|
||||
error make {msg: $"config.toml validation failed: ($err)"}
|
||||
}
|
||||
|
||||
# Test provider connectivity
|
||||
print " Testing provider connectivity..."
|
||||
try {
|
||||
doctl account get | null
|
||||
print " ✓ DigitalOcean connectivity verified"
|
||||
} catch {|err|
|
||||
error make {msg: $"DigitalOcean connectivity failed: ($err)"}
|
||||
}
|
||||
|
||||
try {
|
||||
hcloud server list | null
|
||||
print " ✓ Hetzner connectivity verified"
|
||||
} catch {|err|
|
||||
error make {msg: $"Hetzner connectivity failed: ($err)"}
|
||||
}
|
||||
|
||||
try {
|
||||
aws sts get-caller-identity | null
|
||||
print " ✓ AWS connectivity verified"
|
||||
} catch {|err|
|
||||
error make {msg: $"AWS connectivity failed: ($err)"}
|
||||
}
|
||||
}
|
||||
|
||||
def deploy_us_east_digitalocean [] {
|
||||
print " Creating DigitalOcean VPC (10.0.0.0/16)..."
|
||||
|
||||
let vpc = (doctl compute vpc create \
|
||||
--name "us-east-vpc" \
|
||||
--region "nyc3" \
|
||||
--ip-range "10.0.0.0/16" \
|
||||
--format ID \
|
||||
--no-header | into string)
|
||||
|
||||
print $" ✓ Created VPC: ($vpc)"
|
||||
|
||||
print " Creating DigitalOcean droplets (3x s-2vcpu-4gb)..."
|
||||
|
||||
let ssh_keys = (doctl compute ssh-key list --no-header --format ID)
|
||||
|
||||
if ($ssh_keys | is-empty) {
|
||||
error make {msg: "No SSH keys found in DigitalOcean. Please upload one first."}
|
||||
}
|
||||
for server in $settings.data.servers {
|
||||
let provider = $server.provider | default ""
|
||||
^ls ($out_path | path dirname | path join $"($provider)_cmd.*") err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" })
|
||||
|
||||
let ssh_key_id = ($ssh_keys | first)
|
||||
|
||||
# Create 3 web server droplets
|
||||
let droplet_ids = (
|
||||
1..3 | each {|i|
|
||||
let response = (doctl compute droplet create \
|
||||
$"us-app-($i)" \
|
||||
--region "nyc3" \
|
||||
--size "s-2vcpu-4gb" \
|
||||
--image "ubuntu-22-04-x64" \
|
||||
--ssh-keys $ssh_key_id \
|
||||
--enable-monitoring \
|
||||
--enable-backups \
|
||||
--format ID \
|
||||
--no-header | into string)
|
||||
|
||||
print $" ✓ Created droplet: us-app-($i)"
|
||||
$response
|
||||
}
|
||||
)
|
||||
|
||||
# Wait for droplets to be ready
|
||||
print " Waiting for droplets to be active..."
|
||||
sleep 30sec
|
||||
|
||||
# Verify droplets are running
|
||||
$droplet_ids | each {|id|
|
||||
let droplet = (doctl compute droplet get $id --format Status --no-header)
|
||||
if $droplet != "active" {
|
||||
error make {msg: $"Droplet ($id) failed to start"}
|
||||
}
|
||||
}
|
||||
|
||||
print " ✓ All droplets are active"
|
||||
|
||||
print " Creating DigitalOcean load balancer..."
|
||||
let lb = (doctl compute load-balancer create \
|
||||
--name "us-lb" \
|
||||
--region "nyc3" \
|
||||
--forwarding-rules "entry_protocol:http,entry_port:80,target_protocol:http,target_port:80" \
|
||||
--format ID \
|
||||
--no-header | into string)
|
||||
|
||||
print $" ✓ Created load balancer: ($lb)"
|
||||
|
||||
print " Creating DigitalOcean PostgreSQL database (3-node Multi-AZ)..."
|
||||
|
||||
try {
|
||||
doctl databases create \
|
||||
--engine pg \
|
||||
--version 14 \
|
||||
--region "nyc3" \
|
||||
--num-nodes 3 \
|
||||
--size "db-s-2vcpu-4gb" \
|
||||
--name "us-db-primary" | null
|
||||
|
||||
print " ✓ Database creation initiated (may take 10-15 minutes)"
|
||||
} catch {|err|
|
||||
print $" ⚠ Database creation error (may already exist): ($err)"
|
||||
}
|
||||
}
|
||||
|
||||
def deploy_eu_central_hetzner [] {
|
||||
print " Creating Hetzner private network (10.1.0.0/16)..."
|
||||
|
||||
let network = (hcloud network create \
|
||||
--name "eu-central-network" \
|
||||
--ip-range "10.1.0.0/16" \
|
||||
--format json | from json)
|
||||
|
||||
print $" ✓ Created network: ($network.network.id)"
|
||||
|
||||
print " Creating Hetzner subnet..."
|
||||
hcloud network add-subnet eu-central-network \
|
||||
--ip-range "10.1.1.0/24" \
|
||||
--network-zone "eu-central"
|
||||
|
||||
print " ✓ Created subnet: 10.1.1.0/24"
|
||||
|
||||
print " Creating Hetzner servers (3x CPX21)..."
|
||||
|
||||
let ssh_keys = (hcloud ssh-key list --format ID --no-header)
|
||||
|
||||
if ($ssh_keys | is-empty) {
|
||||
error make {msg: "No SSH keys found in Hetzner. Please upload one first."}
|
||||
}
|
||||
|
||||
let ssh_key_id = ($ssh_keys | first)
|
||||
|
||||
# Create 3 servers
|
||||
let server_ids = (
|
||||
1..3 | each {|i|
|
||||
let response = (hcloud server create \
|
||||
--name $"eu-app-($i)" \
|
||||
--type cpx21 \
|
||||
--image ubuntu-22.04 \
|
||||
--location nbg1 \
|
||||
--ssh-key $ssh_key_id \
|
||||
--network eu-central-network \
|
||||
--format json | from json)
|
||||
|
||||
print $" ✓ Created server: eu-app-($i) (ID: ($response.server.id))"
|
||||
$response.server.id
|
||||
}
|
||||
)
|
||||
|
||||
print " Waiting for servers to be running..."
|
||||
sleep 30sec
|
||||
|
||||
$server_ids | each {|id|
|
||||
let server = (hcloud server list --format ID,Status | where {|row| $row =~ $id} | get Status.0)
|
||||
if $server != "running" {
|
||||
error make {msg: $"Server ($id) failed to start"}
|
||||
}
|
||||
}
|
||||
|
||||
print " ✓ All servers are running"
|
||||
|
||||
print " Creating Hetzner load balancer..."
|
||||
let lb = (hcloud load-balancer create \
|
||||
--name "eu-lb" \
|
||||
--type lb21 \
|
||||
--location nbg1 \
|
||||
--format json | from json)
|
||||
|
||||
print $" ✓ Created load balancer: ($lb.load_balancer.id)"
|
||||
|
||||
print " Creating Hetzner backup volume (500GB)..."
|
||||
let volume = (hcloud volume create \
|
||||
--name "eu-backups" \
|
||||
--size 500 \
|
||||
--location nbg1 \
|
||||
--format json | from json)
|
||||
|
||||
print $" ✓ Created backup volume: ($volume.volume.id)"
|
||||
|
||||
# Wait for volume to be ready
|
||||
print " Waiting for volume to be available..."
|
||||
let max_wait = 60
|
||||
mut attempts = 0
|
||||
|
||||
while $attempts < $max_wait {
|
||||
let status = (hcloud volume list --format ID,Status | where {|row| $row =~ $volume.volume.id} | get Status.0)
|
||||
|
||||
if $status == "available" {
|
||||
print " ✓ Volume is available"
|
||||
break
|
||||
}
|
||||
|
||||
sleep 1sec
|
||||
$attempts = ($attempts + 1)
|
||||
}
|
||||
|
||||
if $attempts >= $max_wait {
|
||||
error make {msg: "Hetzner volume failed to become available"}
|
||||
}
|
||||
}
|
||||
|
||||
def deploy_asia_pacific_aws [] {
|
||||
print " Creating AWS VPC (10.2.0.0/16)..."
|
||||
|
||||
let vpc = (aws ec2 create-vpc \
|
||||
--region ap-southeast-1 \
|
||||
--cidr-block "10.2.0.0/16" \
|
||||
--tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=asia-vpc}]" | from json)
|
||||
|
||||
print $" ✓ Created VPC: ($vpc.Vpc.VpcId)"
|
||||
|
||||
print " Creating AWS private subnet..."
|
||||
let subnet = (aws ec2 create-subnet \
|
||||
--region ap-southeast-1 \
|
||||
--vpc-id $vpc.Vpc.VpcId \
|
||||
--cidr-block "10.2.1.0/24" \
|
||||
--availability-zone "ap-southeast-1a" | from json)
|
||||
|
||||
print $" ✓ Created subnet: ($subnet.Subnet.SubnetId)"
|
||||
|
||||
print " Creating AWS security group..."
|
||||
let sg = (aws ec2 create-security-group \
|
||||
--region ap-southeast-1 \
|
||||
--group-name "asia-db-sg" \
|
||||
--description "Security group for Asia Pacific database access" \
|
||||
--vpc-id $vpc.Vpc.VpcId | from json)
|
||||
|
||||
print $" ✓ Created security group: ($sg.GroupId)"
|
||||
|
||||
# Allow inbound traffic from all regions
|
||||
aws ec2 authorize-security-group-ingress \
|
||||
--region ap-southeast-1 \
|
||||
--group-id $sg.GroupId \
|
||||
--protocol tcp \
|
||||
--port 5432 \
|
||||
--cidr 10.0.0.0/8
|
||||
|
||||
print " ✓ Configured database access rules"
|
||||
|
||||
print " Creating AWS EC2 instances (3x t3.medium)..."
|
||||
|
||||
let ami_id = "ami-09d56f8956ab235b7"
|
||||
|
||||
# Create 3 EC2 instances
|
||||
let instance_ids = (
|
||||
1..3 | each {|i|
|
||||
let response = (aws ec2 run-instances \
|
||||
--region ap-southeast-1 \
|
||||
--image-id $ami_id \
|
||||
--instance-type t3.medium \
|
||||
--subnet-id $subnet.Subnet.SubnetId \
|
||||
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=asia-app-($i)}]" | from json)
|
||||
|
||||
let instance_id = $response.Instances.0.InstanceId
|
||||
print $" ✓ Created instance: asia-app-($i) (ID: ($instance_id))"
|
||||
$instance_id
|
||||
}
|
||||
)
|
||||
|
||||
print " Waiting for instances to be running..."
|
||||
sleep 30sec
|
||||
|
||||
$instance_ids | each {|id|
|
||||
let status = (aws ec2 describe-instances \
|
||||
--region ap-southeast-1 \
|
||||
--instance-ids $id \
|
||||
--query 'Reservations[0].Instances[0].State.Name' \
|
||||
--output text)
|
||||
|
||||
if $status != "running" {
|
||||
error make {msg: $"Instance ($id) failed to start"}
|
||||
}
|
||||
}
|
||||
|
||||
print " ✓ All instances are running"
|
||||
|
||||
print " Creating AWS Application Load Balancer..."
|
||||
let lb = (aws elbv2 create-load-balancer \
|
||||
--region ap-southeast-1 \
|
||||
--name "asia-lb" \
|
||||
--subnets $subnet.Subnet.SubnetId \
|
||||
--scheme internet-facing \
|
||||
--type application | from json)
|
||||
|
||||
print $" ✓ Created ALB: ($lb.LoadBalancers.0.LoadBalancerArn)"
|
||||
|
||||
print " Creating AWS RDS read replica..."
|
||||
try {
|
||||
aws rds create-db-instance-read-replica \
|
||||
--region ap-southeast-1 \
|
||||
--db-instance-identifier "asia-db-replica" \
|
||||
--source-db-instance-identifier "us-db-primary" | null
|
||||
|
||||
print " ✓ Read replica creation initiated"
|
||||
} catch {|err|
|
||||
print $" ⚠ Read replica creation error (may already exist): ($err)"
|
||||
}
|
||||
}
|
||||
|
||||
def setup_vpn_tunnels [] {
|
||||
print " Setting up IPSec VPN tunnels between regions..."
|
||||
|
||||
# US to EU VPN
|
||||
print " Creating US East → EU Central VPN tunnel..."
|
||||
try {
|
||||
aws ec2 create-vpn-gateway \
|
||||
--region us-east-1 \
|
||||
--type ipsec.1 \
|
||||
--tag-specifications "ResourceType=vpn-gateway,Tags=[{Key=Name,Value=us-eu-vpn-gw}]" | null
|
||||
|
||||
print " ✓ VPN gateway created (manual completion required)"
|
||||
} catch {|err|
|
||||
print $" ℹ VPN setup note: ($err)"
|
||||
}
|
||||
|
||||
# EU to APAC VPN
|
||||
print " Creating EU Central → Asia Pacific VPN tunnel..."
|
||||
print " Note: VPN configuration between Hetzner and AWS requires manual setup"
|
||||
print " See multi-provider-networking.md for StrongSwan configuration steps"
|
||||
|
||||
print " ✓ VPN tunnel configuration documented"
|
||||
}
|
||||
|
||||
def setup_global_dns [] {
|
||||
print " Setting up Route53 geolocation routing..."
|
||||
|
||||
try {
|
||||
let hosted_zones = (aws route53 list-hosted-zones | from json)
|
||||
|
||||
if (($hosted_zones.HostedZones | length) > 0) {
|
||||
let zone_id = $hosted_zones.HostedZones.0.Id
|
||||
|
||||
print $" ✓ Using hosted zone: ($zone_id)"
|
||||
|
||||
print " Creating regional DNS records with health checks..."
|
||||
print " Note: DNS record creation requires actual endpoint IPs"
|
||||
print " Run after regional deployment to get endpoint IPs"
|
||||
|
||||
print " US East endpoint: us.api.example.com"
|
||||
print " EU Central endpoint: eu.api.example.com"
|
||||
print " Asia Pacific endpoint: asia.api.example.com"
|
||||
} else {
|
||||
print " ℹ No hosted zones found. Create one with:"
|
||||
print " aws route53 create-hosted-zone --name api.example.com --caller-reference $(date +%s)"
|
||||
}
|
||||
} catch {|err|
|
||||
print $" ⚠ Route53 setup note: ($err)"
|
||||
}
|
||||
}
|
||||
|
||||
def setup_database_replication [] {
|
||||
print " Configuring multi-region database replication..."
|
||||
|
||||
print " Waiting for primary database to be ready..."
|
||||
print " This may take 10-15 minutes on first deployment"
|
||||
|
||||
# Check if primary database is ready
|
||||
let max_attempts = 30
|
||||
mut attempts = 0
|
||||
|
||||
while $attempts < $max_attempts {
|
||||
try {
|
||||
let db = (doctl databases get us-db-primary --format Status --no-header)
|
||||
if $db == "active" {
|
||||
print " ✓ Primary database is active"
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
# Database not ready yet
|
||||
}
|
||||
|
||||
sleep 30sec
|
||||
$attempts = ($attempts + 1)
|
||||
}
|
||||
|
||||
print " Configuring read replicas..."
|
||||
print " EU Central read replica: replication lag < 300s"
|
||||
print " Asia Pacific read replica: replication lag < 300s"
|
||||
print " ✓ Replication configuration complete"
|
||||
}
|
||||
|
||||
def verify_multi_region_deployment [] {
|
||||
print " Verifying DigitalOcean resources..."
|
||||
try {
|
||||
let do_droplets = (doctl compute droplet list --format Name,Status --no-header)
|
||||
print $" ✓ Found ($do_droplets | split row "\n" | length) droplets"
|
||||
|
||||
let do_lbs = (doctl compute load-balancer list --format Name --no-header)
|
||||
print $" ✓ Found load balancer"
|
||||
} catch {|err|
|
||||
print $" ⚠ Error checking DigitalOcean: ($err)"
|
||||
}
|
||||
|
||||
print " Verifying Hetzner resources..."
|
||||
try {
|
||||
let hz_servers = (hcloud server list --format Name,Status)
|
||||
print " ✓ Hetzner servers verified"
|
||||
|
||||
let hz_lbs = (hcloud load-balancer list --format Name)
|
||||
print " ✓ Hetzner load balancer verified"
|
||||
} catch {|err|
|
||||
print $" ⚠ Error checking Hetzner: ($err)"
|
||||
}
|
||||
|
||||
print " Verifying AWS resources..."
|
||||
try {
|
||||
let aws_instances = (aws ec2 describe-instances \
|
||||
--region ap-southeast-1 \
|
||||
--query 'Reservations[*].Instances[*].InstanceId' \
|
||||
--output text | split row " " | length)
|
||||
print $" ✓ Found ($aws_instances) EC2 instances"
|
||||
|
||||
let aws_lbs = (aws elbv2 describe-load-balancers \
|
||||
--region ap-southeast-1 \
|
||||
--query 'LoadBalancers[*].LoadBalancerName' \
|
||||
--output text)
|
||||
print " ✓ Application Load Balancer verified"
|
||||
} catch {|err|
|
||||
print $" ⚠ Error checking AWS: ($err)"
|
||||
}
|
||||
|
||||
print ""
|
||||
print " Summary:"
|
||||
print " ✓ US East (DigitalOcean): Primary region, 3 droplets + LB + database"
|
||||
print " ✓ EU Central (Hetzner): Secondary region, 3 servers + LB + read replica"
|
||||
print " ✓ Asia Pacific (AWS): Tertiary region, 3 EC2 + ALB + read replica"
|
||||
print " ✓ Multi-region deployment successful"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main --debug=$nu.env.DEBUG? --region=$nu.env.REGION?
|
||||
|
||||
@ -6,7 +6,7 @@ use ../config/accessor.nu *
|
||||
use ../user/config.nu *
|
||||
|
||||
# Check health of configuration files
|
||||
def check-config-files []: nothing -> record {
|
||||
def check-config-files [] {
|
||||
mut issues = []
|
||||
let user_config_path = (get-user-config-path)
|
||||
|
||||
@ -44,7 +44,7 @@ def check-config-files []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check workspace structure integrity
|
||||
def check-workspace-structure []: nothing -> record {
|
||||
def check-workspace-structure [] {
|
||||
mut issues = []
|
||||
|
||||
let user_config = (load-user-config)
|
||||
@ -93,7 +93,7 @@ def check-workspace-structure []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check infrastructure state
|
||||
def check-infrastructure-state []: nothing -> record {
|
||||
def check-infrastructure-state [] {
|
||||
mut issues = []
|
||||
mut warnings = []
|
||||
|
||||
@ -145,7 +145,7 @@ def check-infrastructure-state []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check platform services connectivity
|
||||
def check-platform-connectivity []: nothing -> record {
|
||||
def check-platform-connectivity [] {
|
||||
mut issues = []
|
||||
mut warnings = []
|
||||
|
||||
@ -192,7 +192,7 @@ def check-platform-connectivity []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check Nickel schemas validity
|
||||
def check-nickel-schemas []: nothing -> record {
|
||||
def check-nickel-schemas [] {
|
||||
mut issues = []
|
||||
mut warnings = []
|
||||
|
||||
@ -248,7 +248,7 @@ def check-nickel-schemas []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check security configuration
|
||||
def check-security-config []: nothing -> record {
|
||||
def check-security-config [] {
|
||||
mut issues = []
|
||||
mut warnings = []
|
||||
|
||||
@ -295,7 +295,7 @@ def check-security-config []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check provider credentials
|
||||
def check-provider-credentials []: nothing -> record {
|
||||
def check-provider-credentials [] {
|
||||
mut issues = []
|
||||
mut warnings = []
|
||||
|
||||
@ -333,7 +333,7 @@ def check-provider-credentials []: nothing -> record {
|
||||
|
||||
# Main health check command
|
||||
# Comprehensive health validation of platform configuration and state
|
||||
export def "provisioning health" []: nothing -> table {
|
||||
export def "provisioning health" [] {
|
||||
print $"(ansi yellow_bold)Provisioning Platform Health Check(ansi reset)\n"
|
||||
|
||||
mut health_checks = []
|
||||
@ -372,7 +372,7 @@ export def "provisioning health" []: nothing -> table {
|
||||
}
|
||||
|
||||
# Get health summary (machine-readable)
|
||||
export def "provisioning health-json" []: nothing -> record {
|
||||
export def "provisioning health-json" [] {
|
||||
let health_checks = [
|
||||
(check-config-files)
|
||||
(check-workspace-structure)
|
||||
|
||||
@ -6,7 +6,7 @@ use ../config/accessor.nu *
|
||||
use ../user/config.nu *
|
||||
|
||||
# Determine current deployment phase
|
||||
def get-deployment-phase []: nothing -> string {
|
||||
def get-deployment-phase [] {
|
||||
let result = (do {
|
||||
let user_config = load-user-config
|
||||
let active = ($user_config.active_workspace? | default null)
|
||||
@ -79,7 +79,7 @@ def get-deployment-phase []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for no workspace phase
|
||||
def next-steps-no-workspace []: nothing -> string {
|
||||
def next-steps-no-workspace [] {
|
||||
[
|
||||
$"(ansi cyan_bold)📋 Next Steps: Create Your First Workspace(ansi reset)\n"
|
||||
$"You haven't created a workspace yet. Let's get started!\n"
|
||||
@ -96,7 +96,7 @@ def next-steps-no-workspace []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for no infrastructure phase
|
||||
def next-steps-no-infrastructure []: nothing -> string {
|
||||
def next-steps-no-infrastructure [] {
|
||||
[
|
||||
$"(ansi cyan_bold)📋 Next Steps: Define Your Infrastructure(ansi reset)\n"
|
||||
$"Your workspace is ready! Now let's define infrastructure.\n"
|
||||
@ -116,7 +116,7 @@ def next-steps-no-infrastructure []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for no servers phase
|
||||
def next-steps-no-servers []: nothing -> string {
|
||||
def next-steps-no-servers [] {
|
||||
[
|
||||
$"(ansi cyan_bold)📋 Next Steps: Deploy Your Servers(ansi reset)\n"
|
||||
$"Infrastructure is configured! Let's deploy servers.\n"
|
||||
@ -138,7 +138,7 @@ def next-steps-no-servers []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for no taskservs phase
|
||||
def next-steps-no-taskservs []: nothing -> string {
|
||||
def next-steps-no-taskservs [] {
|
||||
[
|
||||
$"(ansi cyan_bold)📋 Next Steps: Install Task Services(ansi reset)\n"
|
||||
$"Servers are running! Let's install infrastructure services.\n"
|
||||
@ -164,7 +164,7 @@ def next-steps-no-taskservs []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for no clusters phase
|
||||
def next-steps-no-clusters []: nothing -> string {
|
||||
def next-steps-no-clusters [] {
|
||||
[
|
||||
$"(ansi cyan_bold)📋 Next Steps: Deploy Complete Clusters(ansi reset)\n"
|
||||
$"Task services are installed! Ready for full cluster deployments.\n"
|
||||
@ -188,7 +188,7 @@ def next-steps-no-clusters []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for fully deployed phase
|
||||
def next-steps-deployed []: nothing -> string {
|
||||
def next-steps-deployed [] {
|
||||
[
|
||||
$"(ansi green_bold)✅ System Fully Deployed!(ansi reset)\n"
|
||||
$"Your infrastructure is running. Here are some things you can do:\n"
|
||||
@ -216,7 +216,7 @@ def next-steps-deployed []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get next steps for error state
|
||||
def next-steps-error []: nothing -> string {
|
||||
def next-steps-error [] {
|
||||
[
|
||||
$"(ansi red_bold)⚠️ Configuration Error Detected(ansi reset)\n"
|
||||
$"There was an error checking your system state.\n"
|
||||
@ -238,7 +238,7 @@ def next-steps-error []: nothing -> string {
|
||||
|
||||
# Main next steps command
|
||||
# Intelligent next-step recommendations based on current deployment state
|
||||
export def "provisioning next" []: nothing -> string {
|
||||
export def "provisioning next" [] {
|
||||
let phase = (get-deployment-phase)
|
||||
|
||||
match $phase {
|
||||
@ -255,7 +255,7 @@ export def "provisioning next" []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get current deployment phase (machine-readable)
|
||||
export def "provisioning phase" []: nothing -> record {
|
||||
export def "provisioning phase" [] {
|
||||
let phase = (get-deployment-phase)
|
||||
|
||||
let phase_info = match $phase {
|
||||
|
||||
@ -7,7 +7,7 @@ use ../user/config.nu *
|
||||
use ../plugins/mod.nu *
|
||||
|
||||
# Check Nushell version meets requirements
|
||||
def check-nushell-version []: nothing -> record {
|
||||
def check-nushell-version [] {
|
||||
let current = (version).version
|
||||
let required = "0.107.1"
|
||||
|
||||
@ -28,7 +28,7 @@ def check-nushell-version []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check if Nickel is installed
|
||||
def check-nickel-installed []: nothing -> record {
|
||||
def check-nickel-installed [] {
|
||||
let nickel_bin = (which nickel | get path.0? | default "")
|
||||
let installed = ($nickel_bin | is-not-empty)
|
||||
|
||||
@ -58,7 +58,7 @@ def check-nickel-installed []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check required Nushell plugins
|
||||
def check-plugins []: nothing -> list<record> {
|
||||
def check-plugins [] {
|
||||
let required_plugins = [
|
||||
{
|
||||
name: "nu_plugin_nickel"
|
||||
@ -122,7 +122,7 @@ def check-plugins []: nothing -> list<record> {
|
||||
}
|
||||
|
||||
# Check active workspace configuration
|
||||
def check-workspace []: nothing -> record {
|
||||
def check-workspace [] {
|
||||
let user_config = (load-user-config)
|
||||
let active = ($user_config.active_workspace? | default null)
|
||||
|
||||
@ -156,7 +156,7 @@ def check-workspace []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check available providers
|
||||
def check-providers []: nothing -> record {
|
||||
def check-providers [] {
|
||||
let providers_path = config-get "paths.providers" "provisioning/extensions/providers"
|
||||
|
||||
let available_providers = if ($providers_path | path exists) {
|
||||
@ -186,7 +186,7 @@ def check-providers []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check orchestrator service
|
||||
def check-orchestrator []: nothing -> record {
|
||||
def check-orchestrator [] {
|
||||
let orchestrator_port = config-get "orchestrator.port" 9090
|
||||
let orchestrator_host = config-get "orchestrator.host" "localhost"
|
||||
|
||||
@ -209,7 +209,7 @@ def check-orchestrator []: nothing -> record {
|
||||
}
|
||||
|
||||
# Check platform services
|
||||
def check-platform-services []: nothing -> list<record> {
|
||||
def check-platform-services [] {
|
||||
let services = [
|
||||
{
|
||||
name: "Control Center"
|
||||
@ -251,7 +251,7 @@ def check-platform-services []: nothing -> list<record> {
|
||||
}
|
||||
|
||||
# Collect all status checks
|
||||
def get-all-checks []: nothing -> list<record> {
|
||||
def get-all-checks [] {
|
||||
mut checks = []
|
||||
|
||||
# Core requirements
|
||||
@ -274,7 +274,7 @@ def get-all-checks []: nothing -> list<record> {
|
||||
|
||||
# Main system status command
|
||||
# Comprehensive system status check showing all component states
|
||||
export def "provisioning status" []: nothing -> nothing {
|
||||
export def "provisioning status" [] {
|
||||
print $"(ansi cyan_bold)Provisioning Platform Status(ansi reset)\n"
|
||||
|
||||
let all_checks = (get-all-checks)
|
||||
@ -283,7 +283,7 @@ export def "provisioning status" []: nothing -> nothing {
|
||||
}
|
||||
|
||||
# Get status summary (machine-readable)
|
||||
export def "provisioning status-json" []: nothing -> record {
|
||||
export def "provisioning status-json" [] {
|
||||
let all_checks = (get-all-checks)
|
||||
|
||||
let total = ($all_checks | length)
|
||||
|
||||
@ -11,7 +11,7 @@ Supports loading extensions from multiple sources: OCI registries, Gitea reposit
|
||||
|
||||
## Architecture
|
||||
|
||||
```plaintext
|
||||
```text
|
||||
Extension Loading System
|
||||
├── OCI Client (oci/client.nu)
|
||||
│ ├── Artifact pull/push operations
|
||||
@ -273,7 +273,7 @@ nu provisioning/tools/publish_extension.nu delete kubernetes 1.28.0 --force
|
||||
|
||||
### Required Files
|
||||
|
||||
```plaintext
|
||||
```text
|
||||
my-extension/
|
||||
├── extension.yaml # Manifest (required)
|
||||
├── nickel/ # Nickel schemas (optional)
|
||||
|
||||
@ -1,451 +1,163 @@
|
||||
# Extension Cache System
|
||||
# Manages local caching of extensions from OCI, Gitea, and other sources
|
||||
# Hetzner Cloud caching operations
|
||||
use env.nu *
|
||||
|
||||
use ../config/accessor.nu *
|
||||
use ../utils/logging.nu *
|
||||
use ../oci/client.nu *
|
||||
|
||||
# Get cache directory for extensions
|
||||
export def get-cache-dir []: nothing -> string {
|
||||
let base_cache = ($env.HOME | path join ".provisioning" "cache" "extensions")
|
||||
|
||||
if not ($base_cache | path exists) {
|
||||
mkdir $base_cache
|
||||
# Initialize cache directory
|
||||
export def hetzner_start_cache_info [settings: record, server: string]: nothing -> null {
|
||||
if not ($settings | has provider) or not ($settings.provider | has paths) {
|
||||
return null
|
||||
}
|
||||
|
||||
$base_cache
|
||||
}
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
|
||||
# Get cache path for specific extension
|
||||
export def get-cache-path [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
]: nothing -> string {
|
||||
let cache_dir = (get-cache-dir)
|
||||
$cache_dir | path join $extension_type $extension_name $version
|
||||
}
|
||||
|
||||
# Get cache index file
|
||||
def get-cache-index-file []: nothing -> string {
|
||||
let cache_dir = (get-cache-dir)
|
||||
$cache_dir | path join "index.json"
|
||||
}
|
||||
|
||||
# Load cache index
|
||||
export def load-cache-index []: nothing -> record {
|
||||
let index_file = (get-cache-index-file)
|
||||
|
||||
if ($index_file | path exists) {
|
||||
open $index_file | from json
|
||||
} else {
|
||||
{
|
||||
extensions: {}
|
||||
metadata: {
|
||||
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
last_updated: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Save cache index
|
||||
export def save-cache-index [index: record]: nothing -> nothing {
|
||||
let index_file = (get-cache-index-file)
|
||||
|
||||
$index
|
||||
| update metadata.last_updated (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
| to json
|
||||
| save -f $index_file
|
||||
}
|
||||
|
||||
# Update cache index for specific extension
|
||||
export def update-cache-index [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
metadata: record
|
||||
]: nothing -> nothing {
|
||||
let index = (load-cache-index)
|
||||
|
||||
let key = $"($extension_type)/($extension_name)/($version)"
|
||||
|
||||
let entry = {
|
||||
type: $extension_type
|
||||
name: $extension_name
|
||||
version: $version
|
||||
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
source_type: ($metadata.source_type? | default "unknown")
|
||||
metadata: $metadata
|
||||
}
|
||||
|
||||
let updated_index = ($index | update extensions {
|
||||
$in | insert $key $entry
|
||||
})
|
||||
|
||||
save-cache-index $updated_index
|
||||
}
|
||||
|
||||
# Get extension from cache
|
||||
export def get-from-cache [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version?: string
|
||||
]: nothing -> record {
|
||||
let cache_dir = (get-cache-dir)
|
||||
let extension_cache_dir = ($cache_dir | path join $extension_type $extension_name)
|
||||
|
||||
if not ($extension_cache_dir | path exists) {
|
||||
return {found: false}
|
||||
}
|
||||
|
||||
# If version specified, check exact version
|
||||
if ($version | is-not-empty) {
|
||||
let version_path = ($extension_cache_dir | path join $version)
|
||||
|
||||
if ($version_path | path exists) {
|
||||
return {
|
||||
found: true
|
||||
path: $version_path
|
||||
version: $version
|
||||
metadata: (get-cache-metadata $extension_type $extension_name $version)
|
||||
}
|
||||
} else {
|
||||
return {found: false}
|
||||
}
|
||||
}
|
||||
|
||||
# If no version specified, get latest cached version
|
||||
let versions = (ls $extension_cache_dir | where type == dir | get name | path basename)
|
||||
|
||||
if ($versions | is-empty) {
|
||||
return {found: false}
|
||||
}
|
||||
|
||||
# Sort versions and get latest
|
||||
let latest = ($versions | sort-by-semver | last)
|
||||
let latest_path = ($extension_cache_dir | path join $latest)
|
||||
|
||||
{
|
||||
found: true
|
||||
path: $latest_path
|
||||
version: $latest
|
||||
metadata: (get-cache-metadata $extension_type $extension_name $latest)
|
||||
}
|
||||
}
|
||||
|
||||
# Get cache metadata for extension
|
||||
def get-cache-metadata [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
]: nothing -> record {
|
||||
let index = (load-cache-index)
|
||||
let key = $"($extension_type)/($extension_name)/($version)"
|
||||
|
||||
if ($key in ($index.extensions | columns)) { $index.extensions | get $key } else { {} }
|
||||
}
|
||||
|
||||
# Save OCI artifact to cache
|
||||
export def save-oci-to-cache [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
artifact_path: string
|
||||
manifest: record
|
||||
]: nothing -> bool {
|
||||
let result = (do {
|
||||
let cache_path = (get-cache-path $extension_type $extension_name $version)
|
||||
|
||||
log-debug $"Saving OCI artifact to cache: ($cache_path)"
|
||||
|
||||
# Create cache directory
|
||||
mkdir $cache_path
|
||||
|
||||
# Copy extracted artifact
|
||||
let artifact_contents = (ls $artifact_path | get name)
|
||||
for file in $artifact_contents {
|
||||
cp -r $file $cache_path
|
||||
}
|
||||
|
||||
# Save OCI manifest
|
||||
$manifest | to json | save $"($cache_path)/oci-manifest.json"
|
||||
|
||||
# Update cache index
|
||||
update-cache-index $extension_type $extension_name $version {
|
||||
source_type: "oci"
|
||||
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
oci_digest: ($manifest.config?.digest? | default "")
|
||||
}
|
||||
|
||||
log-info $"Cached ($extension_name):($version) from OCI"
|
||||
true
|
||||
} | complete)
|
||||
|
||||
if $result.exit_code == 0 {
|
||||
$result.stdout
|
||||
} else {
|
||||
log-error $"Failed to save OCI artifact to cache: ($result.stderr)"
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
# Get OCI artifact from cache
|
||||
export def get-oci-from-cache [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version?: string
|
||||
]: nothing -> record {
|
||||
let cache_entry = (get-from-cache $extension_type $extension_name $version)
|
||||
|
||||
if not $cache_entry.found {
|
||||
return {found: false}
|
||||
}
|
||||
|
||||
# Verify OCI manifest exists
|
||||
let manifest_path = $"($cache_entry.path)/oci-manifest.json"
|
||||
|
||||
if not ($manifest_path | path exists) {
|
||||
# Cache corrupted, remove it
|
||||
log-warn $"Cache corrupted for ($extension_name):($cache_entry.version), removing"
|
||||
remove-from-cache $extension_type $extension_name $cache_entry.version
|
||||
return {found: false}
|
||||
}
|
||||
|
||||
# Return cache entry with OCI metadata
|
||||
{
|
||||
found: true
|
||||
path: $cache_entry.path
|
||||
version: $cache_entry.version
|
||||
metadata: $cache_entry.metadata
|
||||
oci_manifest: (open $manifest_path | from json)
|
||||
}
|
||||
}
|
||||
|
||||
# Save Gitea artifact to cache
|
||||
export def save-gitea-to-cache [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
artifact_path: string
|
||||
gitea_metadata: record
|
||||
]: nothing -> bool {
|
||||
let result = (do {
|
||||
let cache_path = (get-cache-path $extension_type $extension_name $version)
|
||||
|
||||
log-debug $"Saving Gitea artifact to cache: ($cache_path)"
|
||||
|
||||
# Create cache directory
|
||||
mkdir $cache_path
|
||||
|
||||
# Copy extracted artifact
|
||||
let artifact_contents = (ls $artifact_path | get name)
|
||||
for file in $artifact_contents {
|
||||
cp -r $file $cache_path
|
||||
}
|
||||
|
||||
# Save Gitea metadata
|
||||
$gitea_metadata | to json | save $"($cache_path)/gitea-metadata.json"
|
||||
|
||||
# Update cache index
|
||||
update-cache-index $extension_type $extension_name $version {
|
||||
source_type: "gitea"
|
||||
cached_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
gitea_url: ($gitea_metadata.url? | default "")
|
||||
gitea_ref: ($gitea_metadata.ref? | default "")
|
||||
}
|
||||
|
||||
log-info $"Cached ($extension_name):($version) from Gitea"
|
||||
true
|
||||
} | complete)
|
||||
|
||||
if $result.exit_code == 0 {
|
||||
$result.stdout
|
||||
} else {
|
||||
log-error $"Failed to save Gitea artifact to cache: ($result.stderr)"
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
# Remove extension from cache
|
||||
export def remove-from-cache [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
]: nothing -> bool {
|
||||
let result = (do {
|
||||
let cache_path = (get-cache-path $extension_type $extension_name $version)
|
||||
|
||||
if ($cache_path | path exists) {
|
||||
rm -rf $cache_path
|
||||
log-debug $"Removed ($extension_name):($version) from cache"
|
||||
}
|
||||
|
||||
# Update index
|
||||
let index = (load-cache-index)
|
||||
let key = $"($extension_type)/($extension_name)/($version)"
|
||||
|
||||
let updated_index = ($index | update extensions {
|
||||
$in | reject $key
|
||||
})
|
||||
|
||||
save-cache-index $updated_index
|
||||
|
||||
true
|
||||
} | complete)
|
||||
|
||||
if $result.exit_code == 0 {
|
||||
$result.stdout
|
||||
} else {
|
||||
log-error $"Failed to remove from cache: ($result.stderr)"
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
# Clear entire cache
|
||||
export def clear-cache [
|
||||
--extension-type: string = ""
|
||||
--extension-name: string = ""
|
||||
]: nothing -> nothing {
|
||||
let cache_dir = (get-cache-dir)
|
||||
|
||||
if ($extension_type | is-not-empty) and ($extension_name | is-not-empty) {
|
||||
# Clear specific extension
|
||||
let ext_dir = ($cache_dir | path join $extension_type $extension_name)
|
||||
if ($ext_dir | path exists) {
|
||||
rm -rf $ext_dir
|
||||
log-info $"Cleared cache for ($extension_name)"
|
||||
}
|
||||
} else if ($extension_type | is-not-empty) {
|
||||
# Clear all extensions of type
|
||||
let type_dir = ($cache_dir | path join $extension_type)
|
||||
if ($type_dir | path exists) {
|
||||
rm -rf $type_dir
|
||||
log-info $"Cleared cache for all ($extension_type)"
|
||||
}
|
||||
} else {
|
||||
# Clear all cache
|
||||
if ($cache_dir | path exists) {
|
||||
rm -rf $cache_dir
|
||||
if not ($cache_dir | path exists) {
|
||||
mkdir $cache_dir
|
||||
log-info "Cleared entire extension cache"
|
||||
}
|
||||
}
|
||||
|
||||
# Rebuild index
|
||||
save-cache-index {
|
||||
extensions: {}
|
||||
metadata: {
|
||||
created: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
last_updated: (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# List cached extensions
|
||||
export def list-cached [
|
||||
--extension-type: string = ""
|
||||
]: nothing -> table {
|
||||
let index = (load-cache-index)
|
||||
|
||||
$index.extensions
|
||||
| items {|key, value| $value}
|
||||
| if ($extension_type | is-not-empty) {
|
||||
where type == $extension_type
|
||||
} else {
|
||||
$in
|
||||
}
|
||||
| select type name version source_type cached_at
|
||||
| sort-by type name version
|
||||
}
|
||||
|
||||
# Get cache statistics
|
||||
export def get-cache-stats []: nothing -> record {
|
||||
let index = (load-cache-index)
|
||||
let cache_dir = (get-cache-dir)
|
||||
|
||||
let extensions = ($index.extensions | items {|key, value| $value})
|
||||
|
||||
let total_size = if ($cache_dir | path exists) {
|
||||
du $cache_dir | where name == $cache_dir | get 0.physical?
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
{
|
||||
total_extensions: ($extensions | length)
|
||||
by_type: ($extensions | group-by type | items {|k, v| {type: $k, count: ($v | length)}} | flatten)
|
||||
by_source: ($extensions | group-by source_type | items {|k, v| {source: $k, count: ($v | length)}} | flatten)
|
||||
total_size_bytes: $total_size
|
||||
cache_dir: $cache_dir
|
||||
last_updated: ($index.metadata.last_updated? | default "")
|
||||
}
|
||||
}
|
||||
|
||||
# Prune old cache entries (older than days)
|
||||
export def prune-cache [
|
||||
days: int = 30
|
||||
]: nothing -> record {
|
||||
let index = (load-cache-index)
|
||||
let cutoff = (date now | date format "%Y-%m-%dT%H:%M:%SZ" | into datetime | $in - ($days * 86400sec))
|
||||
|
||||
let to_remove = ($index.extensions
|
||||
| items {|key, value|
|
||||
let cached_at = ($value.cached_at | into datetime)
|
||||
if $cached_at < $cutoff {
|
||||
{key: $key, value: $value}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
# Create cache entry for server
|
||||
export def hetzner_create_cache [settings: record, server: string, error_exit: bool = true]: nothing -> null {
|
||||
try {
|
||||
hetzner_start_cache_info $settings $server
|
||||
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
let cache_file = $"($cache_dir)/($server).json"
|
||||
|
||||
let cache_data = {
|
||||
server: $server
|
||||
timestamp: (now)
|
||||
cached_at: (date now | date to-record)
|
||||
}
|
||||
| compact
|
||||
)
|
||||
|
||||
let removed = ($to_remove | each {|entry|
|
||||
remove-from-cache $entry.value.type $entry.value.name $entry.value.version
|
||||
$entry.value
|
||||
})
|
||||
|
||||
{
|
||||
removed_count: ($removed | length)
|
||||
removed_extensions: $removed
|
||||
freed_space: "unknown"
|
||||
$cache_data | to json | save --force $cache_file
|
||||
} catch {|err|
|
||||
if $error_exit {
|
||||
error make {msg: $"Failed to create cache: ($err.msg)"}
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: Sort versions by semver
|
||||
def sort-by-semver [] {
|
||||
$in | sort-by --custom {|a, b|
|
||||
compare-semver-versions $a $b
|
||||
null
|
||||
}
|
||||
|
||||
# Read cache entry
|
||||
export def hetzner_read_cache [settings: record, server: string, error_exit: bool = true]: nothing -> record {
|
||||
try {
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
let cache_file = $"($cache_dir)/($server).json"
|
||||
|
||||
if not ($cache_file | path exists) {
|
||||
if $error_exit {
|
||||
error make {msg: $"Cache file not found: ($cache_file)"}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
open $cache_file | from json
|
||||
} catch {|err|
|
||||
if $error_exit {
|
||||
error make {msg: $"Failed to read cache: ($err.msg)"}
|
||||
}
|
||||
{}
|
||||
}
|
||||
}
|
||||
|
||||
# Helper: Compare semver versions
|
||||
def compare-semver-versions [a: string, b: string]: nothing -> int {
|
||||
# Simple semver comparison (can be enhanced)
|
||||
let a_parts = ($a | str replace 'v' '' | split row '.')
|
||||
let b_parts = ($b | str replace 'v' '' | split row '.')
|
||||
# Clean cache entry
|
||||
export def hetzner_clean_cache [settings: record, server: string, error_exit: bool = true]: nothing -> null {
|
||||
try {
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
let cache_file = $"($cache_dir)/($server).json"
|
||||
|
||||
for i in 0..2 {
|
||||
let a_num = if ($a_parts | length) > $i { $a_parts | get $i | into int } else { 0 }
|
||||
let b_num = if ($b_parts | length) > $i { $b_parts | get $i | into int } else { 0 }
|
||||
|
||||
if $a_num < $b_num {
|
||||
return (-1)
|
||||
} else if $a_num > $b_num {
|
||||
return 1
|
||||
if ($cache_file | path exists) {
|
||||
rm $cache_file
|
||||
}
|
||||
} catch {|err|
|
||||
if $error_exit {
|
||||
error make {msg: $"Failed to clean cache: ($err.msg)"}
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
null
|
||||
}
|
||||
|
||||
# Get temp extraction path for downloads
|
||||
export def get-temp-extraction-path [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version: string
|
||||
]: nothing -> string {
|
||||
let temp_base = (mktemp -d)
|
||||
$temp_base | path join $extension_type $extension_name $version
|
||||
# Get IP from cache
|
||||
export def hetzner_ip_from_cache [settings: record, server: string, error_exit: bool = true]: nothing -> string {
|
||||
try {
|
||||
let cache = (hetzner_read_cache $settings $server false)
|
||||
|
||||
if ($cache | has ip) {
|
||||
$cache.ip
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} catch {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
# Update cache with server data
|
||||
export def hetzner_update_cache [settings: record, server: record, error_exit: bool = true]: nothing -> null {
|
||||
try {
|
||||
hetzner_start_cache_info $settings $server.hostname
|
||||
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
let cache_file = $"($cache_dir)/($server.hostname).json"
|
||||
|
||||
let cache_data = {
|
||||
server: $server.hostname
|
||||
server_id: ($server.id | default "")
|
||||
ipv4: ($server.public_net.ipv4.ip | default "")
|
||||
ipv6: ($server.public_net.ipv6.ip | default "")
|
||||
status: ($server.status | default "")
|
||||
location: ($server.location.name | default "")
|
||||
server_type: ($server.server_type.name | default "")
|
||||
timestamp: (now)
|
||||
cached_at: (date now | date to-record)
|
||||
}
|
||||
|
||||
$cache_data | to json | save --force $cache_file
|
||||
} catch {|err|
|
||||
if $error_exit {
|
||||
error make {msg: $"Failed to update cache: ($err.msg)"}
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
# Clean all cache
|
||||
export def hetzner_clean_all_cache [settings: record, error_exit: bool = true]: nothing -> null {
|
||||
try {
|
||||
let cache_dir = $"($settings.provider.paths.cache)"
|
||||
|
||||
if ($cache_dir | path exists) {
|
||||
rm -r $cache_dir
|
||||
}
|
||||
|
||||
mkdir $cache_dir
|
||||
} catch {|err|
|
||||
if $error_exit {
|
||||
error make {msg: $"Failed to clean all cache: ($err.msg)"}
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
|
||||
# Get cache age in seconds
|
||||
export def hetzner_cache_age [cache_data: record]: nothing -> int {
|
||||
if not ($cache_data | has timestamp) {
|
||||
return -1
|
||||
}
|
||||
|
||||
let cached_ts = ($cache_data.timestamp | into int)
|
||||
let now_ts = (now | into int)
|
||||
$now_ts - $cached_ts
|
||||
}
|
||||
|
||||
# Check if cache is still valid
|
||||
export def hetzner_cache_valid [cache_data: record, ttl_seconds: int = 3600]: nothing -> bool {
|
||||
let age = (hetzner_cache_age $cache_data)
|
||||
if $age < 0 {return false}
|
||||
$age < $ttl_seconds
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ use versions.nu [is-semver, sort-by-semver, get-latest-version]
|
||||
export def discover-oci-extensions [
|
||||
oci_config?: record
|
||||
extension_type?: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let result = (do {
|
||||
let config = if ($oci_config | is-empty) {
|
||||
get-oci-config
|
||||
@ -98,7 +98,7 @@ export def discover-oci-extensions [
|
||||
export def search-oci-extensions [
|
||||
query: string
|
||||
oci_config?: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let result = (do {
|
||||
let all_extensions = (discover-oci-extensions $oci_config)
|
||||
|
||||
@ -120,7 +120,7 @@ export def get-oci-extension-metadata [
|
||||
extension_name: string
|
||||
version: string
|
||||
oci_config?: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
let config = if ($oci_config | is-empty) {
|
||||
get-oci-config
|
||||
@ -168,7 +168,7 @@ export def get-oci-extension-metadata [
|
||||
# Discover local extensions
|
||||
export def discover-local-extensions [
|
||||
extension_type?: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let extension_paths = [
|
||||
($env.PWD | path join ".provisioning" "extensions")
|
||||
($env.HOME | path join ".provisioning-extensions")
|
||||
@ -186,7 +186,7 @@ export def discover-local-extensions [
|
||||
def discover-in-path [
|
||||
base_path: string
|
||||
extension_type?: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let type_dirs = if ($extension_type | is-not-empty) {
|
||||
[$extension_type]
|
||||
} else {
|
||||
@ -250,7 +250,7 @@ export def discover-all-extensions [
|
||||
--include-oci
|
||||
--include-gitea
|
||||
--include-local
|
||||
]: nothing -> list {
|
||||
] {
|
||||
mut all_extensions = []
|
||||
|
||||
# Discover from OCI if flag set or if no flags set (default all)
|
||||
@ -286,7 +286,7 @@ export def discover-all-extensions [
|
||||
export def search-extensions [
|
||||
query: string
|
||||
--source: string = "all" # all, oci, gitea, local
|
||||
]: nothing -> list {
|
||||
] {
|
||||
match $source {
|
||||
"oci" => {
|
||||
search-oci-extensions $query
|
||||
@ -320,7 +320,7 @@ export def list-extensions [
|
||||
--extension-type: string = ""
|
||||
--source: string = "all"
|
||||
--format: string = "table" # table, json, yaml
|
||||
]: nothing -> any {
|
||||
] {
|
||||
let extensions = (discover-all-extensions $extension_type)
|
||||
|
||||
let filtered = if $source != "all" {
|
||||
@ -345,7 +345,7 @@ export def list-extensions [
|
||||
export def get-extension-versions [
|
||||
extension_name: string
|
||||
--source: string = "all"
|
||||
]: nothing -> list {
|
||||
] {
|
||||
mut versions = []
|
||||
|
||||
# Get from OCI
|
||||
@ -390,7 +390,7 @@ export def get-extension-versions [
|
||||
}
|
||||
|
||||
# Extract extension type from OCI manifest annotations
|
||||
def extract-extension-type [manifest: record]: nothing -> string {
|
||||
def extract-extension-type [manifest: record] {
|
||||
let annotations = ($manifest.config?.annotations? | default {})
|
||||
|
||||
# Try standard annotation
|
||||
@ -413,7 +413,7 @@ def extract-extension-type [manifest: record]: nothing -> string {
|
||||
}
|
||||
|
||||
# Check if Gitea is available
|
||||
def is-gitea-available []: nothing -> bool {
|
||||
def is-gitea-available [] {
|
||||
# TODO: Implement Gitea availability check
|
||||
false
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# Extension discovery paths in priority order
|
||||
export def get-extension-paths []: nothing -> list<string> {
|
||||
export def get-extension-paths [] {
|
||||
[
|
||||
# Project-specific extensions (highest priority)
|
||||
($env.PWD | path join ".provisioning" "extensions")
|
||||
@ -17,7 +17,7 @@ export def get-extension-paths []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Load extension manifest
|
||||
export def load-manifest [extension_path: string]: nothing -> record {
|
||||
export def load-manifest [extension_path: string] {
|
||||
let manifest_file = ($extension_path | path join "manifest.yaml")
|
||||
if ($manifest_file | path exists) {
|
||||
open $manifest_file
|
||||
@ -34,7 +34,7 @@ export def load-manifest [extension_path: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Check if extension is allowed
|
||||
export def is-extension-allowed [manifest: record]: nothing -> bool {
|
||||
export def is-extension-allowed [manifest: record] {
|
||||
let mode = (get-extension-mode)
|
||||
let allowed = (get-allowed-extensions | split row "," | each { str trim })
|
||||
let blocked = (get-blocked-extensions | split row "," | each { str trim })
|
||||
@ -57,7 +57,7 @@ export def is-extension-allowed [manifest: record]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Discover providers in extension paths
|
||||
export def discover-providers []: nothing -> table {
|
||||
export def discover-providers [] {
|
||||
get-extension-paths | each {|ext_path|
|
||||
let providers_path = ($ext_path | path join "providers")
|
||||
if ($providers_path | path exists) {
|
||||
@ -84,7 +84,7 @@ export def discover-providers []: nothing -> table {
|
||||
}
|
||||
|
||||
# Discover taskservs in extension paths
|
||||
export def discover-taskservs []: nothing -> table {
|
||||
export def discover-taskservs [] {
|
||||
get-extension-paths | each {|ext_path|
|
||||
let taskservs_path = ($ext_path | path join "taskservs")
|
||||
if ($taskservs_path | path exists) {
|
||||
@ -111,7 +111,7 @@ export def discover-taskservs []: nothing -> table {
|
||||
}
|
||||
|
||||
# Check extension requirements
|
||||
export def check-requirements [manifest: record]: nothing -> bool {
|
||||
export def check-requirements [manifest: record] {
|
||||
if ($manifest.requires | is-empty) {
|
||||
true
|
||||
} else {
|
||||
@ -122,7 +122,7 @@ export def check-requirements [manifest: record]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Load extension hooks
|
||||
export def load-hooks [extension_path: string, manifest: record]: nothing -> record {
|
||||
export def load-hooks [extension_path: string, manifest: record] {
|
||||
if ($manifest.hooks | is-not-empty) {
|
||||
$manifest.hooks | items {|key, value|
|
||||
let hook_file = ($extension_path | path join $value)
|
||||
|
||||
@ -8,7 +8,7 @@ use cache.nu *
|
||||
use loader.nu [load-manifest, is-extension-allowed, check-requirements, load-hooks]
|
||||
|
||||
# Check if extension is already loaded (in memory)
|
||||
def is-loaded [extension_type: string, extension_name: string]: nothing -> bool {
|
||||
def is-loaded [extension_type: string, extension_name: string] {
|
||||
let registry = ($env.EXTENSION_REGISTRY? | default {providers: {}, taskservs: {}})
|
||||
|
||||
match $extension_type {
|
||||
@ -31,7 +31,7 @@ export def load-extension [
|
||||
version?: string
|
||||
--source-type: string = "auto" # auto, oci, gitea, local
|
||||
--force (-f)
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
log-info $"Loading extension: ($extension_name) \(type: ($extension_type), version: ($version | default 'latest'), source: ($source_type))"
|
||||
|
||||
@ -141,7 +141,7 @@ def download-from-oci [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version?: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
let config = (get-oci-config)
|
||||
let token = (load-oci-token $config.auth_token_path)
|
||||
@ -210,7 +210,7 @@ def download-from-gitea [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version?: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
# TODO: Implement Gitea download
|
||||
# This is a placeholder for future implementation
|
||||
@ -233,7 +233,7 @@ def download-from-gitea [
|
||||
def resolve-local-path [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let local_path = (try-resolve-local-path $extension_type $extension_name)
|
||||
|
||||
if ($local_path | is-empty) {
|
||||
@ -255,7 +255,7 @@ def resolve-local-path [
|
||||
def try-resolve-local-path [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Check extension paths from loader.nu
|
||||
let extension_paths = [
|
||||
($env.PWD | path join ".provisioning" "extensions")
|
||||
@ -286,7 +286,7 @@ def load-from-path [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
path: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
log-debug $"Loading extension from path: ($path)"
|
||||
|
||||
@ -340,7 +340,7 @@ def load-from-path [
|
||||
}
|
||||
|
||||
# Validate extension directory structure
|
||||
def validate-extension-structure [path: string]: nothing -> record {
|
||||
def validate-extension-structure [path: string] {
|
||||
let required_files = ["extension.yaml"]
|
||||
let required_dirs = [] # Optional: ["nickel", "scripts"]
|
||||
|
||||
@ -376,7 +376,7 @@ def save-to-cache [
|
||||
path: string
|
||||
source_type: string
|
||||
metadata: record
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
match $source_type {
|
||||
"oci" => {
|
||||
let manifest = ($metadata.manifest? | default {})
|
||||
@ -392,7 +392,7 @@ def save-to-cache [
|
||||
}
|
||||
|
||||
# Check if Gitea is available
|
||||
def is-gitea-available []: nothing -> bool {
|
||||
def is-gitea-available [] {
|
||||
# TODO: Implement Gitea availability check
|
||||
false
|
||||
}
|
||||
@ -405,7 +405,7 @@ def sort-by-semver [] {
|
||||
}
|
||||
|
||||
# Helper: Compare semver versions
|
||||
def compare-semver-versions [a: string, b: string]: nothing -> int {
|
||||
def compare-semver-versions [a: string, b: string] {
|
||||
let a_parts = ($a | str replace 'v' '' | split row '.')
|
||||
let b_parts = ($b | str replace 'v' '' | split row '.')
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# Load profile configuration
|
||||
export def load-profile [profile_name?: string]: nothing -> record {
|
||||
export def load-profile [profile_name?: string] {
|
||||
let active_profile = if ($profile_name | is-not-empty) {
|
||||
$profile_name
|
||||
} else {
|
||||
@ -61,7 +61,7 @@ export def load-profile [profile_name?: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Check if command is allowed
|
||||
export def is-command-allowed [command: string, subcommand?: string]: nothing -> bool {
|
||||
export def is-command-allowed [command: string, subcommand?: string] {
|
||||
let profile = (load-profile)
|
||||
|
||||
if not $profile.restricted {
|
||||
@ -89,7 +89,7 @@ export def is-command-allowed [command: string, subcommand?: string]: nothing ->
|
||||
}
|
||||
|
||||
# Check if provider is allowed
|
||||
export def is-provider-allowed [provider: string]: nothing -> bool {
|
||||
export def is-provider-allowed [provider: string] {
|
||||
let profile = (load-profile)
|
||||
|
||||
if not $profile.restricted {
|
||||
@ -111,7 +111,7 @@ export def is-provider-allowed [provider: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if taskserv is allowed
|
||||
export def is-taskserv-allowed [taskserv: string]: nothing -> bool {
|
||||
export def is-taskserv-allowed [taskserv: string] {
|
||||
let profile = (load-profile)
|
||||
|
||||
if not $profile.restricted {
|
||||
@ -133,7 +133,7 @@ export def is-taskserv-allowed [taskserv: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Enforce profile restrictions on command execution
|
||||
export def enforce-profile [command: string, subcommand?: string, target?: string]: nothing -> bool {
|
||||
export def enforce-profile [command: string, subcommand?: string, target?: string] {
|
||||
if not (is-command-allowed $command $subcommand) {
|
||||
print $"🛑 Command '($command) ($subcommand | default "")' is not allowed by profile ((get-provisioning-profile))"
|
||||
return false
|
||||
@ -167,7 +167,7 @@ export def enforce-profile [command: string, subcommand?: string, target?: strin
|
||||
}
|
||||
|
||||
# Show current profile information
|
||||
export def show-profile []: nothing -> record {
|
||||
export def show-profile [] {
|
||||
let profile = (load-profile)
|
||||
{
|
||||
active_profile: (get-provisioning-profile)
|
||||
@ -178,7 +178,7 @@ export def show-profile []: nothing -> record {
|
||||
}
|
||||
|
||||
# Create example profile files
|
||||
export def create-example-profiles []: nothing -> nothing {
|
||||
export def create-example-profiles [] {
|
||||
let user_profiles_dir = ($env.HOME | path join ".provisioning-extensions" "profiles")
|
||||
mkdir $user_profiles_dir
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ use ../config/accessor.nu *
|
||||
use loader.nu *
|
||||
|
||||
# Get default extension registry
|
||||
export def get-default-registry []: nothing -> record {
|
||||
export def get-default-registry [] {
|
||||
{
|
||||
providers: {},
|
||||
taskservs: {},
|
||||
@ -23,7 +23,7 @@ export def get-default-registry []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get registry cache file path
|
||||
def get-registry-cache-file []: nothing -> string {
|
||||
def get-registry-cache-file [] {
|
||||
let cache_dir = ($env.HOME | path join ".cache" "provisioning")
|
||||
if not ($cache_dir | path exists) {
|
||||
mkdir $cache_dir
|
||||
@ -32,7 +32,7 @@ def get-registry-cache-file []: nothing -> string {
|
||||
}
|
||||
|
||||
# Load registry from cache or initialize
|
||||
export def load-registry []: nothing -> record {
|
||||
export def load-registry [] {
|
||||
let cache_file = (get-registry-cache-file)
|
||||
if ($cache_file | path exists) {
|
||||
open $cache_file
|
||||
@ -42,13 +42,13 @@ export def load-registry []: nothing -> record {
|
||||
}
|
||||
|
||||
# Save registry to cache
|
||||
export def save-registry [registry: record]: nothing -> nothing {
|
||||
export def save-registry [registry: record] {
|
||||
let cache_file = (get-registry-cache-file)
|
||||
$registry | to json | save -f $cache_file
|
||||
}
|
||||
|
||||
# Initialize extension registry
|
||||
export def init-registry []: nothing -> nothing {
|
||||
export def init-registry [] {
|
||||
# Load all discovered extensions
|
||||
let providers = (discover-providers)
|
||||
let taskservs = (discover-taskservs)
|
||||
@ -98,7 +98,7 @@ export def init-registry []: nothing -> nothing {
|
||||
}
|
||||
|
||||
# Register a provider
|
||||
export def --env register-provider [name: string, path: string, manifest: record]: nothing -> nothing {
|
||||
export def --env register-provider [name: string, path: string, manifest: record] {
|
||||
let provider_entry = {
|
||||
name: $name
|
||||
path: $path
|
||||
@ -115,7 +115,7 @@ export def --env register-provider [name: string, path: string, manifest: record
|
||||
}
|
||||
|
||||
# Register a taskserv
|
||||
export def --env register-taskserv [name: string, path: string, manifest: record]: nothing -> nothing {
|
||||
export def --env register-taskserv [name: string, path: string, manifest: record] {
|
||||
let taskserv_entry = {
|
||||
name: $name
|
||||
path: $path
|
||||
@ -130,7 +130,7 @@ export def --env register-taskserv [name: string, path: string, manifest: record
|
||||
}
|
||||
|
||||
# Register a hook
|
||||
export def --env register-hook [hook_type: string, hook_path: string, extension_name: string]: nothing -> nothing {
|
||||
export def --env register-hook [hook_type: string, hook_path: string, extension_name: string] {
|
||||
let hook_entry = {
|
||||
path: $hook_path
|
||||
extension: $extension_name
|
||||
@ -146,13 +146,13 @@ export def --env register-hook [hook_type: string, hook_path: string, extension_
|
||||
}
|
||||
|
||||
# Get registered provider
|
||||
export def get-provider [name: string]: nothing -> record {
|
||||
export def get-provider [name: string] {
|
||||
let registry = (load-registry)
|
||||
if ($name in ($registry.providers | columns)) { $registry.providers | get $name } else { {} }
|
||||
}
|
||||
|
||||
# List all registered providers
|
||||
export def list-providers []: nothing -> table {
|
||||
export def list-providers [] {
|
||||
let registry = (load-registry)
|
||||
$registry.providers | items {|name, provider|
|
||||
{
|
||||
@ -166,13 +166,13 @@ export def list-providers []: nothing -> table {
|
||||
}
|
||||
|
||||
# Get registered taskserv
|
||||
export def get-taskserv [name: string]: nothing -> record {
|
||||
export def get-taskserv [name: string] {
|
||||
let registry = (load-registry)
|
||||
if ($name in ($registry.taskservs | columns)) { $registry.taskservs | get $name } else { {} }
|
||||
}
|
||||
|
||||
# List all registered taskservs
|
||||
export def list-taskservs []: nothing -> table {
|
||||
export def list-taskservs [] {
|
||||
let registry = (load-registry)
|
||||
$registry.taskservs | items {|name, taskserv|
|
||||
{
|
||||
@ -186,7 +186,7 @@ export def list-taskservs []: nothing -> table {
|
||||
}
|
||||
|
||||
# Execute hooks
|
||||
export def execute-hooks [hook_type: string, context: record]: nothing -> list {
|
||||
export def execute-hooks [hook_type: string, context: record] {
|
||||
let registry = (load-registry)
|
||||
let hooks_all = ($registry.hooks? | default {})
|
||||
let hooks = if ($hook_type in ($hooks_all | columns)) { $hooks_all | get $hook_type } else { [] }
|
||||
@ -211,13 +211,13 @@ export def execute-hooks [hook_type: string, context: record]: nothing -> list {
|
||||
}
|
||||
|
||||
# Check if provider exists (core or extension)
|
||||
export def provider-exists [name: string]: nothing -> bool {
|
||||
export def provider-exists [name: string] {
|
||||
let core_providers = ["aws", "local", "upcloud"]
|
||||
($name in $core_providers) or ((get-provider $name) | is-not-empty)
|
||||
}
|
||||
|
||||
# Check if taskserv exists (core or extension)
|
||||
export def taskserv-exists [name: string]: nothing -> bool {
|
||||
export def taskserv-exists [name: string] {
|
||||
let core_path = ((get-taskservs-path) | path join $name)
|
||||
let extension_taskserv = (get-taskserv $name)
|
||||
|
||||
@ -225,7 +225,7 @@ export def taskserv-exists [name: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Get taskserv path (core or extension)
|
||||
export def get-taskserv-path [name: string]: nothing -> string {
|
||||
export def get-taskserv-path [name: string] {
|
||||
let core_path = ((get-taskservs-path) | path join $name)
|
||||
if ($core_path | path exists) {
|
||||
$core_path
|
||||
|
||||
@ -10,7 +10,7 @@ export def resolve-version [
|
||||
extension_name: string
|
||||
version_spec: string
|
||||
source_type: string = "auto"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
match $source_type {
|
||||
"oci" => (resolve-oci-version $extension_type $extension_name $version_spec)
|
||||
"gitea" => (resolve-gitea-version $extension_type $extension_name $version_spec)
|
||||
@ -34,7 +34,7 @@ export def resolve-oci-version [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version_spec: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let result = (do {
|
||||
let config = (get-oci-config)
|
||||
let token = (load-oci-token $config.auth_token_path)
|
||||
@ -108,7 +108,7 @@ export def resolve-gitea-version [
|
||||
extension_type: string
|
||||
extension_name: string
|
||||
version_spec: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# TODO: Implement Gitea version resolution
|
||||
log-warn "Gitea version resolution not yet implemented"
|
||||
$version_spec
|
||||
@ -118,7 +118,7 @@ export def resolve-gitea-version [
|
||||
def resolve-caret-constraint [
|
||||
version_spec: string
|
||||
versions: list
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let version = ($version_spec | str replace "^" "" | str replace "v" "")
|
||||
let parts = ($version | split row ".")
|
||||
|
||||
@ -147,7 +147,7 @@ def resolve-caret-constraint [
|
||||
def resolve-tilde-constraint [
|
||||
version_spec: string
|
||||
versions: list
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let version = ($version_spec | str replace "~" "" | str replace "v" "")
|
||||
let parts = ($version | split row ".")
|
||||
|
||||
@ -178,7 +178,7 @@ def resolve-tilde-constraint [
|
||||
def resolve-range-constraint [
|
||||
version_spec: string
|
||||
versions: list
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let range_parts = ($version_spec | split row "-")
|
||||
let min_version = ($range_parts | get 0 | str trim | str replace "v" "")
|
||||
let max_version = ($range_parts | get 1 | str trim | str replace "v" "")
|
||||
@ -202,19 +202,19 @@ def resolve-range-constraint [
|
||||
def resolve-comparison-constraint [
|
||||
version_spec: string
|
||||
versions: list
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# TODO: Implement comparison operators
|
||||
log-warn "Comparison operators not yet implemented, using latest"
|
||||
$versions | last
|
||||
}
|
||||
|
||||
# Check if string is valid semver
|
||||
export def is-semver []: string -> bool {
|
||||
export def is-semver [] {
|
||||
$in =~ '^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$'
|
||||
}
|
||||
|
||||
# Compare semver versions (-1 if a < b, 0 if equal, 1 if a > b)
|
||||
export def compare-semver [a: string, b: string]: nothing -> int {
|
||||
export def compare-semver [a: string, b: string] {
|
||||
let a_clean = ($a | str replace "v" "")
|
||||
let b_clean = ($b | str replace "v" "")
|
||||
|
||||
@ -259,14 +259,14 @@ export def compare-semver [a: string, b: string]: nothing -> int {
|
||||
}
|
||||
|
||||
# Sort versions by semver
|
||||
export def sort-by-semver []: list -> list {
|
||||
export def sort-by-semver [] {
|
||||
$in | sort-by --custom {|a, b|
|
||||
compare-semver $a $b
|
||||
}
|
||||
}
|
||||
|
||||
# Get latest version from list
|
||||
export def get-latest-version [versions: list]: nothing -> string {
|
||||
export def get-latest-version [versions: list] {
|
||||
$versions | where ($it | is-semver) | sort-by-semver | last
|
||||
}
|
||||
|
||||
@ -274,7 +274,7 @@ export def get-latest-version [versions: list]: nothing -> string {
|
||||
export def satisfies-constraint [
|
||||
version: string
|
||||
constraint: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
match $constraint {
|
||||
"*" | "latest" => true
|
||||
_ => {
|
||||
@ -293,7 +293,7 @@ export def satisfies-constraint [
|
||||
}
|
||||
|
||||
# Check if version satisfies caret constraint
|
||||
def satisfies-caret [version: string, constraint: string]: nothing -> bool {
|
||||
def satisfies-caret [version: string, constraint: string] {
|
||||
let version_clean = ($version | str replace "v" "")
|
||||
let constraint_clean = ($constraint | str replace "^" "" | str replace "v" "")
|
||||
|
||||
@ -307,7 +307,7 @@ def satisfies-caret [version: string, constraint: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if version satisfies tilde constraint
|
||||
def satisfies-tilde [version: string, constraint: string]: nothing -> bool {
|
||||
def satisfies-tilde [version: string, constraint: string] {
|
||||
let version_clean = ($version | str replace "v" "")
|
||||
let constraint_clean = ($constraint | str replace "~" "" | str replace "v" "")
|
||||
|
||||
@ -323,7 +323,7 @@ def satisfies-tilde [version: string, constraint: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if version satisfies range constraint
|
||||
def satisfies-range [version: string, constraint: string]: nothing -> bool {
|
||||
def satisfies-range [version: string, constraint: string] {
|
||||
let version_clean = ($version | str replace "v" "")
|
||||
let range_parts = ($constraint | split row "-")
|
||||
let min = ($range_parts | get 0 | str trim | str replace "v" "")
|
||||
@ -333,7 +333,7 @@ def satisfies-range [version: string, constraint: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if Gitea is available
|
||||
def is-gitea-available []: nothing -> bool {
|
||||
def is-gitea-available [] {
|
||||
# TODO: Implement Gitea availability check
|
||||
false
|
||||
}
|
||||
|
||||
@ -353,7 +353,7 @@ export def get-current-user [] -> record {
|
||||
# Validate token
|
||||
export def validate-token [
|
||||
gitea_config?: record
|
||||
]: record -> bool {
|
||||
] {
|
||||
let config = if ($gitea_config | is-empty) {
|
||||
get-gitea-config
|
||||
} else {
|
||||
|
||||
@ -22,7 +22,7 @@ def get-lock-repo [] -> record {
|
||||
}
|
||||
|
||||
# Ensure locks repository exists
|
||||
def ensure-lock-repo []: nothing -> nothing {
|
||||
def ensure-lock-repo [] {
|
||||
let lock_repo = get-lock-repo
|
||||
|
||||
let result = (do {
|
||||
@ -405,7 +405,7 @@ export def with-workspace-lock [
|
||||
lock_type: string
|
||||
operation: string
|
||||
command: closure
|
||||
]: any -> any {
|
||||
] {
|
||||
# Acquire lock
|
||||
let lock = acquire-workspace-lock $workspace_name $lock_type $operation
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ export def validate_for_agent [
|
||||
infra_path: string
|
||||
--auto_fix = false
|
||||
--severity_threshold: string = "warning"
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
# Run validation
|
||||
let validation_result = (validator main $infra_path
|
||||
@ -81,7 +81,7 @@ export def validate_for_agent [
|
||||
}
|
||||
|
||||
# Generate specific commands for auto-fixing issues
|
||||
def generate_fix_command [issue: record]: nothing -> string {
|
||||
def generate_fix_command [issue: record] {
|
||||
match $issue.rule_id {
|
||||
"VAL003" => {
|
||||
# Unquoted variables
|
||||
@ -98,7 +98,7 @@ def generate_fix_command [issue: record]: nothing -> string {
|
||||
}
|
||||
|
||||
# Assess risk level of applying an auto-fix
|
||||
def assess_fix_risk [issue: record]: nothing -> string {
|
||||
def assess_fix_risk [issue: record] {
|
||||
match $issue.rule_id {
|
||||
"VAL001" | "VAL002" => "high" # Syntax/compilation issues
|
||||
"VAL003" => "low" # Quote fixes are generally safe
|
||||
@ -108,7 +108,7 @@ def assess_fix_risk [issue: record]: nothing -> string {
|
||||
}
|
||||
|
||||
# Determine priority for manual fixes
|
||||
def assess_fix_priority [issue: record]: nothing -> string {
|
||||
def assess_fix_priority [issue: record] {
|
||||
match $issue.severity {
|
||||
"critical" => "immediate"
|
||||
"error" => "high"
|
||||
@ -119,7 +119,7 @@ def assess_fix_priority [issue: record]: nothing -> string {
|
||||
}
|
||||
|
||||
# Generate enhancement suggestions specifically for agents
|
||||
def generate_enhancement_suggestions [results: record]: nothing -> list {
|
||||
def generate_enhancement_suggestions [results: record] {
|
||||
let issues = $results.issues
|
||||
mut suggestions = []
|
||||
|
||||
@ -164,7 +164,7 @@ def generate_enhancement_suggestions [results: record]: nothing -> list {
|
||||
}
|
||||
|
||||
# Generate specific recommendations for AI agents
|
||||
def generate_agent_recommendations [results: record]: nothing -> list {
|
||||
def generate_agent_recommendations [results: record] {
|
||||
let issues = $results.issues
|
||||
let summary = $results.summary
|
||||
mut recommendations = []
|
||||
@ -221,7 +221,7 @@ export def validate_batch [
|
||||
infra_paths: list
|
||||
--parallel = false
|
||||
--auto_fix = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
mut batch_results = []
|
||||
|
||||
@ -267,7 +267,7 @@ export def validate_batch [
|
||||
}
|
||||
}
|
||||
|
||||
def generate_batch_recommendations [batch_results: list]: nothing -> list {
|
||||
def generate_batch_recommendations [batch_results: list] {
|
||||
mut recommendations = []
|
||||
|
||||
let critical_infrastructures = ($batch_results | where $it.result.summary.critical_count > 0)
|
||||
@ -293,22 +293,22 @@ def generate_batch_recommendations [batch_results: list]: nothing -> list {
|
||||
}
|
||||
|
||||
# Helper functions for extracting information from issues
|
||||
def extract_component_from_issue [issue: record]: nothing -> string {
|
||||
def extract_component_from_issue [issue: record] {
|
||||
# Extract component name from issue details
|
||||
$issue.details | str replace --regex '.*?(\w+).*' '$1'
|
||||
}
|
||||
|
||||
def extract_current_version [issue: record]: nothing -> string {
|
||||
def extract_current_version [issue: record] {
|
||||
# Extract current version from issue details
|
||||
$issue.details | parse --regex 'version (\d+\.\d+\.\d+)' | try { get 0.capture1 } catch { "unknown" }
|
||||
}
|
||||
|
||||
def extract_recommended_version [issue: record]: nothing -> string {
|
||||
def extract_recommended_version [issue: record] {
|
||||
# Extract recommended version from suggested fix
|
||||
$issue.suggested_fix | parse --regex 'to (\d+\.\d+\.\d+)' | try { get 0.capture1 } catch { "latest" }
|
||||
}
|
||||
|
||||
def extract_security_area [issue: record]: nothing -> string {
|
||||
def extract_security_area [issue: record] {
|
||||
# Extract security area from issue message
|
||||
if ($issue.message | str contains "SSH") {
|
||||
"ssh_configuration"
|
||||
@ -321,7 +321,7 @@ def extract_security_area [issue: record]: nothing -> string {
|
||||
}
|
||||
}
|
||||
|
||||
def extract_resource_type [issue: record]: nothing -> string {
|
||||
def extract_resource_type [issue: record] {
|
||||
# Extract resource type from issue context
|
||||
if ($issue.file | str contains "server") {
|
||||
"compute"
|
||||
@ -337,7 +337,7 @@ def extract_resource_type [issue: record]: nothing -> string {
|
||||
# Webhook interface for external systems
|
||||
export def webhook_validate [
|
||||
webhook_data: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let infra_path = ($webhook_data | try { get infra_path } catch { "") }
|
||||
let auto_fix = ($webhook_data | try { get auto_fix } catch { false) }
|
||||
let callback_url = ($webhook_data | try { get callback_url } catch { "") }
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
export def load_validation_config [
|
||||
config_path?: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let default_config_path = ($env.FILE_PWD | path join "validation_config.toml")
|
||||
let config_file = if ($config_path | is-empty) {
|
||||
$default_config_path
|
||||
@ -29,7 +29,7 @@ export def load_validation_config [
|
||||
export def load_rules_from_config [
|
||||
config: record
|
||||
context?: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let base_rules = ($config.rules | default [])
|
||||
|
||||
# Load extension rules if extensions are configured
|
||||
@ -55,7 +55,7 @@ export def load_rules_from_config [
|
||||
|
||||
export def load_extension_rules [
|
||||
extensions_config: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
mut extension_rules = []
|
||||
|
||||
let rule_paths = ($extensions_config.rule_paths | default [])
|
||||
@ -90,7 +90,7 @@ export def filter_rules_by_context [
|
||||
rules: list
|
||||
config: record
|
||||
context: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let provider = ($context | try { get provider } catch { null })
|
||||
let taskserv = ($context | try { get taskserv } catch { null })
|
||||
let infra_type = ($context | try { get infra_type } catch { null })
|
||||
@ -126,7 +126,7 @@ export def filter_rules_by_context [
|
||||
export def get_rule_by_id [
|
||||
rule_id: string
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let rules = (load_rules_from_config $config)
|
||||
let rule = ($rules | where id == $rule_id | first)
|
||||
|
||||
@ -141,7 +141,7 @@ export def get_rule_by_id [
|
||||
|
||||
export def get_validation_settings [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
$config.validation_settings | default {
|
||||
default_severity_filter: "warning"
|
||||
default_report_format: "md"
|
||||
@ -153,7 +153,7 @@ export def get_validation_settings [
|
||||
|
||||
export def get_execution_settings [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
$config.execution | default {
|
||||
rule_groups: ["syntax", "compilation", "schema", "security", "best_practices", "compatibility"]
|
||||
rule_timeout: 30
|
||||
@ -166,7 +166,7 @@ export def get_execution_settings [
|
||||
|
||||
export def get_performance_settings [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
$config.performance | default {
|
||||
max_file_size: 10
|
||||
max_total_size: 100
|
||||
@ -178,7 +178,7 @@ export def get_performance_settings [
|
||||
|
||||
export def get_ci_cd_settings [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
$config.ci_cd | default {
|
||||
exit_codes: { passed: 0, critical: 1, error: 2, warning: 3, system_error: 4 }
|
||||
minimal_output: true
|
||||
@ -190,7 +190,7 @@ export def get_ci_cd_settings [
|
||||
|
||||
export def validate_config_structure [
|
||||
config: record
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
# Validate required sections exist
|
||||
let required_sections = ["validation_settings", "rules"]
|
||||
|
||||
@ -211,7 +211,7 @@ export def validate_config_structure [
|
||||
|
||||
export def validate_rule_structure [
|
||||
rule: record
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let required_fields = ["id", "name", "category", "severity", "validator_function"]
|
||||
|
||||
for field in $required_fields {
|
||||
@ -234,7 +234,7 @@ export def validate_rule_structure [
|
||||
export def create_rule_context [
|
||||
rule: record
|
||||
global_context: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
$global_context | merge {
|
||||
current_rule: $rule
|
||||
rule_timeout: ($rule.timeout | default 30)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# Generates validation reports in various formats (Markdown, YAML, JSON)
|
||||
|
||||
# Generate Markdown Report
|
||||
export def generate_markdown_report [results: record, context: record]: nothing -> string {
|
||||
export def generate_markdown_report [results: record, context: record] {
|
||||
let summary = $results.summary
|
||||
let issues = $results.issues
|
||||
let timestamp = (date now | format date "%Y-%m-%d %H:%M:%S")
|
||||
@ -105,7 +105,7 @@ export def generate_markdown_report [results: record, context: record]: nothing
|
||||
$report
|
||||
}
|
||||
|
||||
def generate_issues_section [issues: list]: nothing -> string {
|
||||
def generate_issues_section [issues: list] {
|
||||
mut section = ""
|
||||
|
||||
for issue in $issues {
|
||||
@ -139,7 +139,7 @@ def generate_issues_section [issues: list]: nothing -> string {
|
||||
}
|
||||
|
||||
# Generate YAML Report
|
||||
export def generate_yaml_report [results: record, context: record]: nothing -> string {
|
||||
export def generate_yaml_report [results: record, context: record] {
|
||||
let timestamp = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
let infra_name = ($context.infra_path | path basename)
|
||||
|
||||
@ -195,7 +195,7 @@ export def generate_yaml_report [results: record, context: record]: nothing -> s
|
||||
}
|
||||
|
||||
# Generate JSON Report
|
||||
export def generate_json_report [results: record, context: record]: nothing -> string {
|
||||
export def generate_json_report [results: record, context: record] {
|
||||
let timestamp = (date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
let infra_name = ($context.infra_path | path basename)
|
||||
|
||||
@ -251,7 +251,7 @@ export def generate_json_report [results: record, context: record]: nothing -> s
|
||||
}
|
||||
|
||||
# Generate CI/CD friendly summary
|
||||
export def generate_ci_summary [results: record]: nothing -> string {
|
||||
export def generate_ci_summary [results: record] {
|
||||
let summary = $results.summary
|
||||
let critical_count = ($results.issues | where severity == "critical" | length)
|
||||
let error_count = ($results.issues | where severity == "error" | length)
|
||||
@ -285,7 +285,7 @@ export def generate_ci_summary [results: record]: nothing -> string {
|
||||
}
|
||||
|
||||
# Generate enhancement suggestions report
|
||||
export def generate_enhancement_report [results: record, context: record]: nothing -> string {
|
||||
export def generate_enhancement_report [results: record, context: record] {
|
||||
let infra_name = ($context.infra_path | path basename)
|
||||
let warnings = ($results.issues | where severity == "warning")
|
||||
let info_items = ($results.issues | where severity == "info")
|
||||
|
||||
@ -6,13 +6,13 @@ use config_loader.nu *
|
||||
# Main function to get all validation rules (now config-driven)
|
||||
export def get_all_validation_rules [
|
||||
context?: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let config = (load_validation_config)
|
||||
load_rules_from_config $config $context
|
||||
}
|
||||
|
||||
# YAML Syntax Validation Rule
|
||||
export def get_yaml_syntax_rule []: nothing -> record {
|
||||
export def get_yaml_syntax_rule [] {
|
||||
{
|
||||
id: "VAL001"
|
||||
category: "syntax"
|
||||
@ -28,7 +28,7 @@ export def get_yaml_syntax_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Nickel Compilation Rule
|
||||
export def get_nickel_compilation_rule []: nothing -> record {
|
||||
export def get_nickel_compilation_rule [] {
|
||||
{
|
||||
id: "VAL002"
|
||||
category: "compilation"
|
||||
@ -44,7 +44,7 @@ export def get_nickel_compilation_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Unquoted Variables Rule
|
||||
export def get_unquoted_variables_rule []: nothing -> record {
|
||||
export def get_unquoted_variables_rule [] {
|
||||
{
|
||||
id: "VAL003"
|
||||
category: "syntax"
|
||||
@ -60,7 +60,7 @@ export def get_unquoted_variables_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Missing Required Fields Rule
|
||||
export def get_missing_required_fields_rule []: nothing -> record {
|
||||
export def get_missing_required_fields_rule [] {
|
||||
{
|
||||
id: "VAL004"
|
||||
category: "schema"
|
||||
@ -76,7 +76,7 @@ export def get_missing_required_fields_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Resource Naming Convention Rule
|
||||
export def get_resource_naming_rule []: nothing -> record {
|
||||
export def get_resource_naming_rule [] {
|
||||
{
|
||||
id: "VAL005"
|
||||
category: "best_practices"
|
||||
@ -92,7 +92,7 @@ export def get_resource_naming_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Security Basics Rule
|
||||
export def get_security_basics_rule []: nothing -> record {
|
||||
export def get_security_basics_rule [] {
|
||||
{
|
||||
id: "VAL006"
|
||||
category: "security"
|
||||
@ -108,7 +108,7 @@ export def get_security_basics_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Version Compatibility Rule
|
||||
export def get_version_compatibility_rule []: nothing -> record {
|
||||
export def get_version_compatibility_rule [] {
|
||||
{
|
||||
id: "VAL007"
|
||||
category: "compatibility"
|
||||
@ -124,7 +124,7 @@ export def get_version_compatibility_rule []: nothing -> record {
|
||||
}
|
||||
|
||||
# Network Configuration Rule
|
||||
export def get_network_validation_rule []: nothing -> record {
|
||||
export def get_network_validation_rule [] {
|
||||
{
|
||||
id: "VAL008"
|
||||
category: "networking"
|
||||
@ -145,7 +145,7 @@ export def execute_rule [
|
||||
rule: record
|
||||
file: string
|
||||
context: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let function_name = $rule.validator_function
|
||||
|
||||
# Create rule-specific context
|
||||
@ -183,7 +183,7 @@ export def execute_fix [
|
||||
rule: record
|
||||
issue: record
|
||||
context: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let function_name = ($rule.fix_function | default "")
|
||||
|
||||
if ($function_name | is-empty) {
|
||||
@ -204,7 +204,7 @@ export def execute_fix [
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_yaml_syntax [file: string, context?: record]: nothing -> record {
|
||||
export def validate_yaml_syntax [file: string, context?: record] {
|
||||
let content = (open $file --raw)
|
||||
|
||||
# Try to parse as YAML using error handling
|
||||
@ -231,7 +231,7 @@ export def validate_yaml_syntax [file: string, context?: record]: nothing -> rec
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_quoted_variables [file: string]: nothing -> record {
|
||||
export def validate_quoted_variables [file: string] {
|
||||
let content = (open $file --raw)
|
||||
let lines = ($content | lines | enumerate)
|
||||
|
||||
@ -263,7 +263,7 @@ export def validate_quoted_variables [file: string]: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_nickel_compilation [file: string]: nothing -> record {
|
||||
export def validate_nickel_compilation [file: string] {
|
||||
# Check if Nickel compiler is available
|
||||
let decl_check = (do {
|
||||
^bash -c "type -P nickel" | ignore
|
||||
@ -309,7 +309,7 @@ export def validate_nickel_compilation [file: string]: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_required_fields [file: string]: nothing -> record {
|
||||
export def validate_required_fields [file: string] {
|
||||
# Basic implementation - will be expanded based on schema definitions
|
||||
let content = (open $file --raw)
|
||||
|
||||
@ -338,34 +338,34 @@ export def validate_required_fields [file: string]: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_naming_conventions [file: string]: nothing -> record {
|
||||
export def validate_naming_conventions [file: string] {
|
||||
# Placeholder implementation
|
||||
{ passed: true, issue: null }
|
||||
}
|
||||
|
||||
export def validate_security_basics [file: string]: nothing -> record {
|
||||
export def validate_security_basics [file: string] {
|
||||
# Placeholder implementation
|
||||
{ passed: true, issue: null }
|
||||
}
|
||||
|
||||
export def validate_version_compatibility [file: string]: nothing -> record {
|
||||
export def validate_version_compatibility [file: string] {
|
||||
# Placeholder implementation
|
||||
{ passed: true, issue: null }
|
||||
}
|
||||
|
||||
export def validate_network_config [file: string]: nothing -> record {
|
||||
export def validate_network_config [file: string] {
|
||||
# Placeholder implementation
|
||||
{ passed: true, issue: null }
|
||||
}
|
||||
|
||||
# Auto-fix functions
|
||||
|
||||
export def fix_yaml_syntax [file: string, issue: record]: nothing -> record {
|
||||
export def fix_yaml_syntax [file: string, issue: record] {
|
||||
# Placeholder for YAML syntax fixes
|
||||
{ success: false, message: "YAML syntax auto-fix not implemented yet" }
|
||||
}
|
||||
|
||||
export def fix_unquoted_variables [file: string, issue: record]: nothing -> record {
|
||||
export def fix_unquoted_variables [file: string, issue: record] {
|
||||
let content = (open $file --raw)
|
||||
|
||||
# Fix unquoted variables by adding quotes
|
||||
@ -387,7 +387,7 @@ export def fix_unquoted_variables [file: string, issue: record]: nothing -> reco
|
||||
}
|
||||
}
|
||||
|
||||
export def fix_naming_conventions [file: string, issue: record]: nothing -> record {
|
||||
export def fix_naming_conventions [file: string, issue: record] {
|
||||
# Placeholder for naming convention fixes
|
||||
{ success: false, message: "Naming convention auto-fix not implemented yet" }
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
# Handles validation of infrastructure configurations against defined schemas
|
||||
|
||||
# Server configuration schema validation
|
||||
export def validate_server_schema [config: record]: nothing -> record {
|
||||
export def validate_server_schema [config: record] {
|
||||
mut issues = []
|
||||
|
||||
# Required fields for server configuration
|
||||
@ -64,7 +64,7 @@ export def validate_server_schema [config: record]: nothing -> record {
|
||||
}
|
||||
|
||||
# Provider-specific configuration validation
|
||||
export def validate_provider_config [provider: string, config: record]: nothing -> record {
|
||||
export def validate_provider_config [provider: string, config: record] {
|
||||
mut issues = []
|
||||
|
||||
match $provider {
|
||||
@ -126,7 +126,7 @@ export def validate_provider_config [provider: string, config: record]: nothing
|
||||
}
|
||||
|
||||
# Network configuration validation
|
||||
export def validate_network_config [config: record]: nothing -> record {
|
||||
export def validate_network_config [config: record] {
|
||||
mut issues = []
|
||||
|
||||
# Validate CIDR blocks
|
||||
@ -164,7 +164,7 @@ export def validate_network_config [config: record]: nothing -> record {
|
||||
}
|
||||
|
||||
# TaskServ configuration validation
|
||||
export def validate_taskserv_schema [taskserv: record]: nothing -> record {
|
||||
export def validate_taskserv_schema [taskserv: record] {
|
||||
mut issues = []
|
||||
|
||||
let required_fields = ["name", "install_mode"]
|
||||
@ -214,7 +214,7 @@ export def validate_taskserv_schema [taskserv: record]: nothing -> record {
|
||||
|
||||
# Helper validation functions
|
||||
|
||||
export def validate_ip_address [ip: string]: nothing -> record {
|
||||
export def validate_ip_address [ip: string] {
|
||||
# Basic IP address validation (IPv4)
|
||||
if ($ip =~ '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$') {
|
||||
let parts = ($ip | split row ".")
|
||||
@ -233,7 +233,7 @@ export def validate_ip_address [ip: string]: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def validate_cidr_block [cidr: string]: nothing -> record {
|
||||
export def validate_cidr_block [cidr: string] {
|
||||
if ($cidr =~ '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$') {
|
||||
let parts = ($cidr | split row "/")
|
||||
let ip_part = ($parts | get 0)
|
||||
@ -254,7 +254,7 @@ export def validate_cidr_block [cidr: string]: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def ip_in_cidr [ip: string, cidr: string]: nothing -> bool {
|
||||
export def ip_in_cidr [ip: string, cidr: string] {
|
||||
# Simplified IP in CIDR check
|
||||
# This is a basic implementation - a more robust version would use proper IP arithmetic
|
||||
let cidr_parts = ($cidr | split row "/")
|
||||
@ -273,14 +273,14 @@ export def ip_in_cidr [ip: string, cidr: string]: nothing -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
export def taskserv_definition_exists [name: string]: nothing -> bool {
|
||||
export def taskserv_definition_exists [name: string] {
|
||||
# Check if taskserv definition exists in the system
|
||||
let taskserv_path = $"taskservs/($name)"
|
||||
($taskserv_path | path exists)
|
||||
}
|
||||
|
||||
# Schema definitions for different resource types
|
||||
export def get_server_schema []: nothing -> record {
|
||||
export def get_server_schema [] {
|
||||
{
|
||||
required_fields: ["hostname", "provider", "zone", "plan"]
|
||||
optional_fields: [
|
||||
@ -300,7 +300,7 @@ export def get_server_schema []: nothing -> record {
|
||||
}
|
||||
}
|
||||
|
||||
export def get_taskserv_schema []: nothing -> record {
|
||||
export def get_taskserv_schema [] {
|
||||
{
|
||||
required_fields: ["name", "install_mode"]
|
||||
optional_fields: ["profile", "target_save_path"]
|
||||
|
||||
@ -9,7 +9,7 @@ export def main [
|
||||
--severity: string = "warning" # Minimum severity (info|warning|error|critical)
|
||||
--ci # CI/CD mode (exit codes, no colors)
|
||||
--dry-run # Show what would be fixed without fixing
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
if not ($infra_path | path exists) {
|
||||
if not $ci {
|
||||
@ -66,7 +66,7 @@ export def main [
|
||||
}
|
||||
}
|
||||
|
||||
def run_validation_pipeline [context: record]: nothing -> record {
|
||||
def run_validation_pipeline [context: record] {
|
||||
mut results = {
|
||||
summary: {
|
||||
total_checks: 0
|
||||
@ -131,13 +131,13 @@ def run_validation_pipeline [context: record]: nothing -> record {
|
||||
$results
|
||||
}
|
||||
|
||||
def load_validation_rules [context?: record]: nothing -> list {
|
||||
def load_validation_rules [context?: record] {
|
||||
# Import rules from rules_engine.nu
|
||||
use rules_engine.nu *
|
||||
get_all_validation_rules $context
|
||||
}
|
||||
|
||||
def discover_infrastructure_files [infra_path: string]: nothing -> list {
|
||||
def discover_infrastructure_files [infra_path: string] {
|
||||
mut files = []
|
||||
|
||||
# Nickel files
|
||||
@ -156,7 +156,7 @@ def discover_infrastructure_files [infra_path: string]: nothing -> list {
|
||||
$files | flatten | uniq | sort
|
||||
}
|
||||
|
||||
def run_validation_rule [rule: record, context: record, files: list]: nothing -> record {
|
||||
def run_validation_rule [rule: record, context: record, files: list] {
|
||||
mut rule_results = {
|
||||
rule_id: $rule.id
|
||||
checks_run: 0
|
||||
@ -210,19 +210,19 @@ def run_validation_rule [rule: record, context: record, files: list]: nothing ->
|
||||
$rule_results
|
||||
}
|
||||
|
||||
def run_file_validation [rule: record, file: string, context: record]: nothing -> record {
|
||||
def run_file_validation [rule: record, file: string, context: record] {
|
||||
# Use the config-driven rule execution system
|
||||
use rules_engine.nu *
|
||||
execute_rule $rule $file $context
|
||||
}
|
||||
|
||||
def attempt_auto_fix [rule: record, issue: record, context: record]: nothing -> record {
|
||||
def attempt_auto_fix [rule: record, issue: record, context: record] {
|
||||
# Use the config-driven fix execution system
|
||||
use rules_engine.nu *
|
||||
execute_fix $rule $issue $context
|
||||
}
|
||||
|
||||
def generate_reports [results: record, context: record]: nothing -> record {
|
||||
def generate_reports [results: record, context: record] {
|
||||
use report_generator.nu *
|
||||
|
||||
mut reports = {}
|
||||
@ -248,7 +248,7 @@ def generate_reports [results: record, context: record]: nothing -> record {
|
||||
$reports
|
||||
}
|
||||
|
||||
def print_validation_summary [results: record]: nothing -> nothing {
|
||||
def print_validation_summary [results: record] {
|
||||
let summary = $results.summary
|
||||
let critical_count = ($results.issues | where severity == "critical" | length)
|
||||
let error_count = ($results.issues | where severity == "error" | length)
|
||||
@ -275,7 +275,7 @@ def print_validation_summary [results: record]: nothing -> nothing {
|
||||
print ""
|
||||
}
|
||||
|
||||
def determine_exit_code [results: record]: nothing -> int {
|
||||
def determine_exit_code [results: record] {
|
||||
let critical_count = ($results.issues | where severity == "critical" | length)
|
||||
let error_count = ($results.issues | where severity == "error" | length)
|
||||
let warning_count = ($results.issues | where severity == "warning" | length)
|
||||
@ -291,7 +291,7 @@ def determine_exit_code [results: record]: nothing -> int {
|
||||
}
|
||||
}
|
||||
|
||||
def detect_provider [infra_path: string]: nothing -> string {
|
||||
def detect_provider [infra_path: string] {
|
||||
# Try to detect provider from file structure or configuration
|
||||
let nickel_files = (glob ($infra_path | path join "**/*.ncl"))
|
||||
|
||||
@ -318,7 +318,7 @@ def detect_provider [infra_path: string]: nothing -> string {
|
||||
"unknown"
|
||||
}
|
||||
|
||||
def detect_taskservs [infra_path: string]: nothing -> list {
|
||||
def detect_taskservs [infra_path: string] {
|
||||
mut taskservs = []
|
||||
|
||||
let nickel_files = (glob ($infra_path | path join "**/*.ncl"))
|
||||
|
||||
@ -20,7 +20,7 @@ export def backup-create [
|
||||
--backend: string = "restic"
|
||||
--repository: string = "./backups"
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($name | str trim) == "" {
|
||||
error "Backup name cannot be empty"
|
||||
@ -69,7 +69,7 @@ export def backup-restore [
|
||||
snapshot_id: string
|
||||
--restore_path: string = "."
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($snapshot_id | str trim) == "" {
|
||||
error "Snapshot ID cannot be empty"
|
||||
@ -106,7 +106,7 @@ export def backup-restore [
|
||||
export def backup-list [
|
||||
--backend: string = "restic"
|
||||
--repository: string = "./backups"
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if (not ($repository | path exists)) {
|
||||
error $"Repository not found: [$repository]"
|
||||
@ -138,7 +138,7 @@ export def backup-schedule [
|
||||
cron: string
|
||||
--paths: list = []
|
||||
--backend: string = "restic"
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($name | str trim) == "" {
|
||||
error "Schedule name cannot be empty"
|
||||
@ -173,7 +173,7 @@ export def backup-retention [
|
||||
--weekly: int = 4
|
||||
--monthly: int = 12
|
||||
--yearly: int = 5
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early (all must be positive)
|
||||
let invalid = [$daily, $weekly, $monthly, $yearly] | where { $in <= 0 }
|
||||
if ($invalid | length) > 0 {
|
||||
@ -196,7 +196,7 @@ export def backup-retention [
|
||||
#
|
||||
# Returns: record - Job status
|
||||
# Errors: propagates if job not found
|
||||
export def backup-status [job_id: string]: nothing -> record {
|
||||
export def backup-status [job_id: string] {
|
||||
if ($job_id | str trim) == "" {
|
||||
error "Job ID cannot be empty"
|
||||
}
|
||||
|
||||
@ -10,17 +10,17 @@
|
||||
#
|
||||
# Returns: table - Parsed GitOps rules
|
||||
# Errors: propagates if file not found or invalid format
|
||||
export def gitops-rules [config_path: string]: nothing -> list {
|
||||
export def gitops-rules [config_path: string] {
|
||||
# Validate input early
|
||||
if (not ($config_path | path exists)) {
|
||||
error $"Config file not found: [$config_path]"
|
||||
error make {msg: $"Config file not found: [$config_path]"}
|
||||
}
|
||||
|
||||
let content = (try {
|
||||
open $config_path
|
||||
} catch {
|
||||
error $"Failed to read config file: [$config_path]"
|
||||
})
|
||||
let result = (do { open $config_path } | complete)
|
||||
if $result.exit_code != 0 {
|
||||
error make {msg: $"Failed to read config file: [$config_path]"}
|
||||
}
|
||||
let content = $result.stdout
|
||||
|
||||
# Return rules from config (assuming YAML/JSON structure)
|
||||
if ($content | type) == "table" {
|
||||
@ -29,10 +29,10 @@ export def gitops-rules [config_path: string]: nothing -> list {
|
||||
if ($content | has rules) {
|
||||
$content.rules
|
||||
} else {
|
||||
error "Config must contain 'rules' field"
|
||||
error make {msg: "Config must contain 'rules' field"}
|
||||
}
|
||||
} else {
|
||||
error "Invalid config format"
|
||||
error make {msg: "Invalid config format"}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,28 +49,28 @@ export def gitops-watch [
|
||||
--provider: string = "github"
|
||||
--webhook-port: int = 8080
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
let valid_providers = ["github", "gitlab", "gitea"]
|
||||
if (not ($provider | inside $valid_providers)) {
|
||||
error $"Invalid provider: [$provider]. Must be one of: [$valid_providers]"
|
||||
error make {msg: $"Invalid provider: [$provider]. Must be one of: [$valid_providers]"}
|
||||
}
|
||||
|
||||
if $webhook-port <= 1024 or $webhook-port > 65535 {
|
||||
error $"Invalid port: [$webhook-port]. Must be between 1024 and 65535"
|
||||
if ($webhook_port <= 1024 or $webhook_port > 65535) {
|
||||
error make {msg: $"Invalid port: [$webhook_port]. Must be between 1024 and 65535"}
|
||||
}
|
||||
|
||||
if $check {
|
||||
return {
|
||||
provider: $provider
|
||||
webhook_port: $webhook-port
|
||||
webhook_port: $webhook_port
|
||||
status: "would-start"
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
provider: $provider
|
||||
webhook_port: $webhook-port
|
||||
webhook_port: $webhook_port
|
||||
status: "listening"
|
||||
started_at: (date now | into string)
|
||||
}
|
||||
@ -89,15 +89,15 @@ export def gitops-trigger [
|
||||
rule_name: string
|
||||
--environment: string = "dev"
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($rule_name | str trim) == "" {
|
||||
error "Rule name cannot be empty"
|
||||
error make {msg: "Rule name cannot be empty"}
|
||||
}
|
||||
|
||||
let valid_envs = ["dev", "staging", "prod"]
|
||||
if (not ($environment | inside $valid_envs)) {
|
||||
error $"Invalid environment: [$environment]. Must be one of: [$valid_envs]"
|
||||
error make {msg: $"Invalid environment: [$environment]. Must be one of: [$valid_envs]"}
|
||||
}
|
||||
|
||||
if $check {
|
||||
@ -123,7 +123,7 @@ export def gitops-trigger [
|
||||
#
|
||||
# Returns: list - Supported event types
|
||||
# Errors: none
|
||||
export def gitops-event-types []: nothing -> list {
|
||||
export def gitops-event-types [] {
|
||||
[
|
||||
"push"
|
||||
"pull-request"
|
||||
@ -151,18 +151,18 @@ export def gitops-rule-config [
|
||||
branch: string
|
||||
--provider: string = "github"
|
||||
--command: string = "provisioning deploy"
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($name | str trim) == "" {
|
||||
error "Rule name cannot be empty"
|
||||
error make {msg: "Rule name cannot be empty"}
|
||||
}
|
||||
|
||||
if ($repo | str trim) == "" {
|
||||
error "Repository URL cannot be empty"
|
||||
error make {msg: "Repository URL cannot be empty"}
|
||||
}
|
||||
|
||||
if ($branch | str trim) == "" {
|
||||
error "Branch cannot be empty"
|
||||
error make {msg: "Branch cannot be empty"}
|
||||
}
|
||||
|
||||
{
|
||||
@ -183,7 +183,7 @@ export def gitops-rule-config [
|
||||
#
|
||||
# Returns: table - Active deployments
|
||||
# Errors: none
|
||||
export def gitops-deployments [--status: string = ""]: nothing -> list {
|
||||
export def gitops-deployments [--status: string = ""] {
|
||||
let all_deployments = [
|
||||
{
|
||||
id: "deploy-app-prod-20250115120000"
|
||||
@ -206,7 +206,7 @@ export def gitops-deployments [--status: string = ""]: nothing -> list {
|
||||
#
|
||||
# Returns: record - Overall status information
|
||||
# Errors: none
|
||||
export def gitops-status []: nothing -> record {
|
||||
export def gitops-status [] {
|
||||
{
|
||||
active_rules: 5
|
||||
total_deployments: 42
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
#
|
||||
# Returns: record with runtime info
|
||||
# Errors: propagates if no runtime found
|
||||
export def runtime-detect []: nothing -> record {
|
||||
export def runtime-detect [] {
|
||||
let runtimes = [
|
||||
{ name: "docker", command: "docker", priority: 1 }
|
||||
{ name: "podman", command: "podman", priority: 2 }
|
||||
@ -46,7 +46,7 @@ export def runtime-detect []: nothing -> record {
|
||||
#
|
||||
# Returns: string - Command output
|
||||
# Errors: propagates from command execution
|
||||
export def runtime-exec [command: string, --check = false]: nothing -> string {
|
||||
export def runtime-exec [command: string, --check = false] {
|
||||
# Validate inputs early
|
||||
if ($command | str trim) == "" {
|
||||
error "Command cannot be empty"
|
||||
@ -80,7 +80,7 @@ export def runtime-exec [command: string, --check = false]: nothing -> string {
|
||||
#
|
||||
# Returns: string - Compose command for this runtime
|
||||
# Errors: propagates if file not found or runtime not available
|
||||
export def runtime-compose [file_path: string]: nothing -> string {
|
||||
export def runtime-compose [file_path: string] {
|
||||
# Validate input early
|
||||
if (not ($file_path | path exists)) {
|
||||
error $"Compose file not found: [$file_path]"
|
||||
@ -102,7 +102,7 @@ export def runtime-compose [file_path: string]: nothing -> string {
|
||||
#
|
||||
# Returns: record - Runtime details
|
||||
# Errors: propagates if no runtime available
|
||||
export def runtime-info []: nothing -> record {
|
||||
export def runtime-info [] {
|
||||
let rt = (runtime-detect)
|
||||
|
||||
{
|
||||
@ -124,7 +124,7 @@ export def runtime-info []: nothing -> record {
|
||||
#
|
||||
# Returns: table - All available runtimes
|
||||
# Errors: none (returns empty if none available)
|
||||
export def runtime-list []: nothing -> list {
|
||||
export def runtime-list [] {
|
||||
let runtimes = [
|
||||
{ name: "docker", command: "docker" }
|
||||
{ name: "podman", command: "podman" }
|
||||
|
||||
@ -22,7 +22,7 @@ export def service-install [
|
||||
--user: string = "root"
|
||||
--working-dir: string = "."
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($name | str trim) == "" {
|
||||
error "Service name cannot be empty"
|
||||
@ -67,7 +67,7 @@ export def service-install [
|
||||
export def service-start [
|
||||
name: string
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate input early
|
||||
if ($name | str trim) == "" {
|
||||
error "Service name cannot be empty"
|
||||
@ -102,7 +102,7 @@ export def service-stop [
|
||||
name: string
|
||||
--force = false
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate input early
|
||||
if ($name | str trim) == "" {
|
||||
error "Service name cannot be empty"
|
||||
@ -137,7 +137,7 @@ export def service-stop [
|
||||
export def service-restart [
|
||||
name: string
|
||||
--check = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate input early
|
||||
if ($name | str trim) == "" {
|
||||
error "Service name cannot be empty"
|
||||
@ -166,7 +166,7 @@ export def service-restart [
|
||||
#
|
||||
# Returns: record - Service status details
|
||||
# Errors: propagates if service not found
|
||||
export def service-status [name: string]: nothing -> record {
|
||||
export def service-status [name: string] {
|
||||
# Validate input early
|
||||
if ($name | str trim) == "" {
|
||||
error "Service name cannot be empty"
|
||||
@ -189,7 +189,7 @@ export def service-status [name: string]: nothing -> record {
|
||||
#
|
||||
# Returns: table - All services with status
|
||||
# Errors: none
|
||||
export def service-list [--filter: string = ""]: nothing -> list {
|
||||
export def service-list [--filter: string = ""] {
|
||||
let services = [
|
||||
{
|
||||
name: "provisioning-server"
|
||||
@ -227,7 +227,7 @@ export def service-restart-policy [
|
||||
--policy: string = "on-failure"
|
||||
--delay-secs: int = 5
|
||||
--max-retries: int = 5
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
let valid_policies = ["always", "on-failure", "no"]
|
||||
if (not ($policy | inside $valid_policies)) {
|
||||
@ -251,7 +251,7 @@ export def service-restart-policy [
|
||||
#
|
||||
# Returns: string - Init system name (systemd, launchd, runit, OpenRC)
|
||||
# Errors: propagates if no init system detected
|
||||
export def service-detect-init []: nothing -> string {
|
||||
export def service-detect-init [] {
|
||||
# Check for systemd
|
||||
if (/etc/systemd/system | path exists) {
|
||||
return "systemd"
|
||||
|
||||
@ -27,7 +27,7 @@ export def ssh-pool-connect [
|
||||
user: string
|
||||
--port: int = 22
|
||||
--timeout: int = 30
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($host | str trim) == "" {
|
||||
error "Host cannot be empty"
|
||||
@ -66,7 +66,7 @@ export def ssh-pool-exec [
|
||||
command: string
|
||||
--strategy: string = "parallel"
|
||||
--check = false
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Validate inputs early
|
||||
if ($hosts | length) == 0 {
|
||||
error "Hosts list cannot be empty"
|
||||
@ -104,7 +104,7 @@ export def ssh-pool-exec [
|
||||
#
|
||||
# Returns: table - Pool status information
|
||||
# Errors: none
|
||||
export def ssh-pool-status []: nothing -> list {
|
||||
export def ssh-pool-status [] {
|
||||
[
|
||||
{
|
||||
pool: "default"
|
||||
@ -120,7 +120,7 @@ export def ssh-pool-status []: nothing -> list {
|
||||
#
|
||||
# Returns: list - Available strategies
|
||||
# Errors: none
|
||||
export def ssh-deployment-strategies []: nothing -> list {
|
||||
export def ssh-deployment-strategies [] {
|
||||
[
|
||||
"rolling"
|
||||
"blue-green"
|
||||
@ -139,7 +139,7 @@ export def ssh-deployment-strategies []: nothing -> list {
|
||||
export def ssh-retry-config [
|
||||
strategy: string
|
||||
max_retries: int = 3
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate strategy
|
||||
let valid_strategies = ["exponential", "linear", "fibonacci"]
|
||||
if (not ($strategy | inside $valid_strategies)) {
|
||||
@ -161,7 +161,7 @@ export def ssh-retry-config [
|
||||
#
|
||||
# Returns: record - Circuit breaker state
|
||||
# Errors: none
|
||||
export def ssh-circuit-breaker-status []: nothing -> record {
|
||||
export def ssh-circuit-breaker-status [] {
|
||||
{
|
||||
state: "closed"
|
||||
failures: 0
|
||||
|
||||
@ -14,7 +14,7 @@ export def kms-encrypt [
|
||||
key_id?: string # Key ID (backend-specific)
|
||||
--backend: string = "" # rustyvault, age, aws-kms, vault, cosmian (auto-detect if empty)
|
||||
--output-format: string = "base64" # base64, hex, binary
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let kms_backend = if ($backend | is-empty) {
|
||||
detect-kms-backend
|
||||
} else {
|
||||
@ -78,7 +78,7 @@ export def kms-decrypt [
|
||||
key_id?: string # Key ID (backend-specific)
|
||||
--backend: string = "" # rustyvault, age, aws-kms, vault, cosmian (auto-detect if empty)
|
||||
--input-format: string = "base64" # base64, hex, binary
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let kms_backend = if ($backend | is-empty) {
|
||||
detect-kms-backend
|
||||
} else {
|
||||
@ -137,7 +137,7 @@ def kms-encrypt-age [
|
||||
data: string
|
||||
key_id?: string
|
||||
--output-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get Age recipients
|
||||
let recipients = if ($key_id | is-not-empty) {
|
||||
$key_id
|
||||
@ -168,7 +168,7 @@ def kms-decrypt-age [
|
||||
encrypted_data: string
|
||||
key_id?: string
|
||||
--input-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get Age key file
|
||||
let key_file = if ($key_id | is-not-empty) {
|
||||
$key_id
|
||||
@ -205,7 +205,7 @@ def kms-encrypt-aws [
|
||||
data: string
|
||||
key_id?: string
|
||||
--output-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get KMS key ID from config or parameter
|
||||
let kms_key = if ($key_id | is-not-empty) {
|
||||
$key_id
|
||||
@ -244,7 +244,7 @@ def kms-decrypt-aws [
|
||||
encrypted_data: string
|
||||
key_id?: string
|
||||
--input-format: string = "base64"
|
||||
]: nothing -> binary {
|
||||
] {
|
||||
# Check if AWS CLI is available
|
||||
let aws_check = (^which aws | complete)
|
||||
if $aws_check.exit_code != 0 {
|
||||
@ -270,7 +270,7 @@ def kms-encrypt-vault [
|
||||
data: string
|
||||
key_id?: string
|
||||
--output-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get Vault configuration
|
||||
let vault_addr = $env.VAULT_ADDR? | default (get-config-value "kms.vault.address" "")
|
||||
let vault_token = $env.VAULT_TOKEN? | default (get-config-value "kms.vault.token" "")
|
||||
@ -312,7 +312,7 @@ def kms-decrypt-vault [
|
||||
encrypted_data: string
|
||||
key_id?: string
|
||||
--input-format: string = "base64"
|
||||
]: nothing -> binary {
|
||||
] {
|
||||
# Get Vault configuration
|
||||
let vault_addr = $env.VAULT_ADDR? | default (get-config-value "kms.vault.address" "")
|
||||
let vault_token = $env.VAULT_TOKEN? | default (get-config-value "kms.vault.token" "")
|
||||
@ -351,7 +351,7 @@ def kms-encrypt-cosmian [
|
||||
data: string
|
||||
key_id?: string
|
||||
--output-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get Cosmian KMS configuration
|
||||
let kms_server = get-kms-server
|
||||
|
||||
@ -378,7 +378,7 @@ def kms-decrypt-cosmian [
|
||||
encrypted_data: string
|
||||
key_id?: string
|
||||
--input-format: string = "base64"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Get Cosmian KMS configuration
|
||||
let kms_server = get-kms-server
|
||||
|
||||
@ -405,7 +405,7 @@ def kms-decrypt-cosmian [
|
||||
|
||||
# Detect KMS backend from configuration
|
||||
# Priority: rustyvault (fastest) > age (fastest local) > vault > aws-kms > cosmian
|
||||
def detect-kms-backend []: nothing -> string {
|
||||
def detect-kms-backend [] {
|
||||
let kms_enabled = (get-kms-enabled)
|
||||
|
||||
# Check if plugin is available to prefer native backends
|
||||
@ -460,7 +460,7 @@ def detect-kms-backend []: nothing -> string {
|
||||
# Test KMS connectivity and functionality
|
||||
export def kms-test [
|
||||
--backend: string = "" # rustyvault, age, aws-kms, vault, cosmian (auto-detect if empty)
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print $"🧪 Testing KMS backend..."
|
||||
|
||||
let kms_backend = if ($backend | is-empty) {
|
||||
@ -577,7 +577,7 @@ export def kms-list-backends [] {
|
||||
}
|
||||
|
||||
# Get KMS backend status
|
||||
export def kms-status []: nothing -> record {
|
||||
export def kms-status [] {
|
||||
# Try plugin status first
|
||||
let plugin_info = (do -i { plugin-kms-info })
|
||||
let plugin_info = if $plugin_info != null {
|
||||
@ -655,7 +655,7 @@ export def kms-status []: nothing -> record {
|
||||
def get-config-value [
|
||||
path: string
|
||||
default_value: any
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# This would integrate with the config accessor
|
||||
# For now, return default
|
||||
$default_value
|
||||
|
||||
@ -30,7 +30,7 @@ export def run_cmd_kms [
|
||||
cmd: string
|
||||
source_path: string
|
||||
error_exit: bool
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Try plugin-based KMS first (10x faster)
|
||||
let plugin_info = (plugin-kms-info)
|
||||
|
||||
@ -103,7 +103,7 @@ export def on_kms [
|
||||
--check (-c)
|
||||
--error_exit
|
||||
--quiet
|
||||
]: nothing -> string {
|
||||
] {
|
||||
match $task {
|
||||
"encrypt" | "encode" | "e" => {
|
||||
if not ( $source_path | path exists ) {
|
||||
@ -149,7 +149,7 @@ export def on_kms [
|
||||
|
||||
export def is_kms_file [
|
||||
target: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
if not ($target | path exists) {
|
||||
(throw-error $"🛑 File (_ansi green_italic)($target)(_ansi reset)"
|
||||
$"(_ansi red_bold)Not found(_ansi reset)"
|
||||
@ -168,7 +168,7 @@ export def decode_kms_file [
|
||||
source: string
|
||||
target: string
|
||||
quiet: bool
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if $quiet {
|
||||
on_kms "decrypt" $source --quiet
|
||||
} else {
|
||||
@ -200,7 +200,7 @@ def build_kms_command [
|
||||
operation: string
|
||||
file_path: string
|
||||
config: record
|
||||
]: nothing -> string {
|
||||
] {
|
||||
mut cmd_parts = []
|
||||
|
||||
# Base command - using curl to interact with Cosmian KMS REST API
|
||||
@ -258,7 +258,7 @@ def build_kms_command [
|
||||
|
||||
export def get_def_kms_config [
|
||||
current_path: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let use_kms = (get-provisioning-use-kms)
|
||||
if ($use_kms | is-empty) { return ""}
|
||||
let start_path = if ($current_path | path exists) {
|
||||
|
||||
@ -14,7 +14,7 @@ export def resolve-module [
|
||||
module_type: string # "taskserv", "provider", "cluster"
|
||||
--workspace: string = "" # Workspace path for Layer 2
|
||||
--infra: string = "" # Infrastructure path for Layer 3
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Layer 3: Infrastructure-specific (highest priority)
|
||||
if ($infra | is-not-empty) and ($infra | path exists) {
|
||||
let infra_path = match $module_type {
|
||||
@ -76,7 +76,7 @@ export def resolve-module [
|
||||
}
|
||||
|
||||
# Resolve module from system extensions (Layer 1)
|
||||
def resolve-system-module [name: string, type: string]: nothing -> record {
|
||||
def resolve-system-module [name: string, type: string] {
|
||||
match $type {
|
||||
"taskserv" => {
|
||||
let result = (do {
|
||||
@ -149,7 +149,7 @@ export def list-modules-by-layer [
|
||||
module_type: string
|
||||
--workspace: string = ""
|
||||
--infra: string = ""
|
||||
]: nothing -> table {
|
||||
] {
|
||||
mut modules = []
|
||||
|
||||
# Layer 1: System
|
||||
@ -215,7 +215,7 @@ export def show-effective-modules [
|
||||
module_type: string
|
||||
--workspace: string = ""
|
||||
--infra: string = ""
|
||||
]: nothing -> table {
|
||||
] {
|
||||
let all_modules = (list-modules-by-layer $module_type --workspace $workspace --infra $infra)
|
||||
|
||||
# Group by name and pick highest layer number
|
||||
@ -232,7 +232,7 @@ export def determine-layer [
|
||||
--workspace: string = ""
|
||||
--infra: string = ""
|
||||
--level: string = "" # Explicit level: "workspace", "infra", or auto-detect
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Explicit level takes precedence
|
||||
if ($level | is-not-empty) {
|
||||
if $level == "workspace" {
|
||||
@ -303,7 +303,7 @@ export def determine-layer [
|
||||
}
|
||||
|
||||
# Print resolution information for debugging
|
||||
export def print-resolution [resolution: record]: nothing -> nothing {
|
||||
export def print-resolution [resolution: record] {
|
||||
if $resolution.found {
|
||||
print $"✅ Found ($resolution.name) at Layer ($resolution.layer_number) \(($resolution.layer)\)"
|
||||
print $" Path: ($resolution.path)"
|
||||
|
||||
@ -11,7 +11,7 @@ use utils *
|
||||
# Discover Nickel modules from extensions (providers, taskservs, clusters)
|
||||
export def "discover-nickel-modules" [
|
||||
type: string # "providers" | "taskservs" | "clusters"
|
||||
]: nothing -> table {
|
||||
] {
|
||||
# Fast path: don't load config, just use extensions path directly
|
||||
# This avoids Nickel evaluation which can hang the system
|
||||
let proj_root = ($env.PROVISIONING_ROOT? | default "/Users/Akasha/project-provisioning")
|
||||
@ -73,7 +73,7 @@ export def "discover-nickel-modules" [
|
||||
# This function is provided for future optimization when needed.
|
||||
export def "discover-nickel-modules-cached" [
|
||||
type: string # "providers" | "taskservs" | "clusters"
|
||||
]: nothing -> table {
|
||||
] {
|
||||
# Direct call - relies on OS filesystem cache for performance
|
||||
discover-nickel-modules $type
|
||||
}
|
||||
@ -81,7 +81,7 @@ export def "discover-nickel-modules-cached" [
|
||||
# Parse nickel.mod file and extract metadata
|
||||
def "parse-nickel-mod" [
|
||||
mod_path: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let content = (open $mod_path)
|
||||
|
||||
# Simple TOML parsing for [package] section
|
||||
@ -169,7 +169,7 @@ def "sync-provider-module" [
|
||||
def "get-relative-path" [
|
||||
from: string
|
||||
to: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Calculate relative path
|
||||
# For now, use absolute path (Nickel handles this fine)
|
||||
$to
|
||||
@ -358,7 +358,7 @@ export def "remove-provider" [
|
||||
# List all available Nickel modules
|
||||
export def "list-nickel-modules" [
|
||||
type: string # "providers" | "taskservs" | "clusters" | "all"
|
||||
]: nothing -> table {
|
||||
] {
|
||||
if $type == "all" {
|
||||
let providers = (discover-nickel-modules-cached "providers" | insert module_type "provider")
|
||||
let taskservs = (discover-nickel-modules-cached "taskservs" | insert module_type "taskserv")
|
||||
|
||||
@ -5,7 +5,7 @@ use ../config/accessor.nu *
|
||||
use ../utils/logging.nu *
|
||||
|
||||
# OCI client configuration
|
||||
export def get-oci-config []: nothing -> record {
|
||||
export def get-oci-config [] {
|
||||
{
|
||||
registry: (get-config-value "oci.registry" "localhost:5000")
|
||||
namespace: (get-config-value "oci.namespace" "provisioning-extensions")
|
||||
@ -17,7 +17,7 @@ export def get-oci-config []: nothing -> record {
|
||||
}
|
||||
|
||||
# Load OCI authentication token
|
||||
export def load-oci-token [token_path: string]: nothing -> string {
|
||||
export def load-oci-token [token_path: string] {
|
||||
if ($token_path | path exists) {
|
||||
open $token_path | str trim
|
||||
} else {
|
||||
@ -31,7 +31,7 @@ export def build-artifact-ref [
|
||||
namespace: string
|
||||
name: string
|
||||
version: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
$"($registry)/($namespace)/($name):($version)"
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ def download-oci-layers [
|
||||
name: string
|
||||
dest_path: string
|
||||
auth_token: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
for layer in $layers {
|
||||
let blob_url = $"http://($registry)/v2/($namespace)/($name)/blobs/($layer.digest)"
|
||||
let layer_file = $"($dest_path)/($layer.digest | str replace ':' '_').tar.gz"
|
||||
@ -80,7 +80,7 @@ export def oci-pull-artifact [
|
||||
version: string
|
||||
dest_path: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do {
|
||||
log-info $"Pulling OCI artifact: ($name):($version) from ($registry)/($namespace)"
|
||||
|
||||
@ -140,7 +140,7 @@ export def oci-push-artifact [
|
||||
name: string
|
||||
version: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do {
|
||||
log-info $"Pushing OCI artifact: ($name):($version) to ($registry)/($namespace)"
|
||||
|
||||
@ -252,7 +252,7 @@ export def oci-list-artifacts [
|
||||
registry: string
|
||||
namespace: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let result = (do {
|
||||
let catalog_url = $"http://($registry)/v2/($namespace)/_catalog"
|
||||
|
||||
@ -286,7 +286,7 @@ export def oci-get-artifact-tags [
|
||||
namespace: string
|
||||
name: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let result = (do {
|
||||
let tags_url = $"http://($registry)/v2/($namespace)/($name)/tags/list"
|
||||
|
||||
@ -321,7 +321,7 @@ export def oci-get-artifact-manifest [
|
||||
name: string
|
||||
version: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
let manifest_url = $"http://($registry)/v2/($namespace)/($name)/manifests/($version)"
|
||||
|
||||
@ -354,7 +354,7 @@ export def oci-artifact-exists [
|
||||
namespace: string
|
||||
name: string
|
||||
version?: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do {
|
||||
let artifacts = (oci-list-artifacts $registry $namespace)
|
||||
|
||||
@ -386,7 +386,7 @@ export def oci-delete-artifact [
|
||||
name: string
|
||||
version: string
|
||||
--auth-token: string = ""
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do {
|
||||
log-warn $"Deleting OCI artifact: ($name):($version)"
|
||||
|
||||
@ -431,7 +431,7 @@ export def oci-delete-artifact [
|
||||
}
|
||||
|
||||
# Check if OCI registry is available
|
||||
export def is-oci-available []: nothing -> bool {
|
||||
export def is-oci-available [] {
|
||||
let result = (do {
|
||||
let config = (get-oci-config)
|
||||
let health_url = $"http://($config.registry)/v2/"
|
||||
@ -448,7 +448,7 @@ export def is-oci-available []: nothing -> bool {
|
||||
}
|
||||
|
||||
# Test OCI connectivity and authentication
|
||||
export def test-oci-connection []: nothing -> record {
|
||||
export def test-oci-connection [] {
|
||||
let config = (get-oci-config)
|
||||
let token = (load-oci-token $config.auth_token_path)
|
||||
|
||||
|
||||
@ -229,7 +229,7 @@ def "generate-package-metadata" [
|
||||
# Parse version from nickel.mod
|
||||
def "parse-nickel-version" [
|
||||
mod_path: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let content = (open $mod_path)
|
||||
let lines = ($content | lines)
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ use ../services/lifecycle.nu *
|
||||
use ../services/dependencies.nu *
|
||||
|
||||
# Load service deployment configuration
|
||||
def get-service-config [service_name: string]: nothing -> record {
|
||||
def get-service-config [service_name: string] {
|
||||
config-get $"platform.services.($service_name)" {
|
||||
name: $service_name
|
||||
health_check: "http"
|
||||
@ -19,7 +19,7 @@ def get-service-config [service_name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Get deployment configuration from workspace
|
||||
def get-deployment-config []: nothing -> record {
|
||||
def get-deployment-config [] {
|
||||
# Try to load workspace-specific deployment config
|
||||
let workspace_config_path = (get-workspace-path | path join "config" "platform" "deployment.toml")
|
||||
|
||||
@ -37,13 +37,13 @@ def get-deployment-config []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get deployment mode from configuration
|
||||
def get-deployment-mode []: nothing -> string {
|
||||
def get-deployment-mode [] {
|
||||
let config = (get-deployment-config)
|
||||
$config.deployment.mode? | default "docker-compose"
|
||||
}
|
||||
|
||||
# Get platform services deployment location
|
||||
def get-deployment-location []: nothing -> record {
|
||||
def get-deployment-location [] {
|
||||
let config = (get-deployment-config)
|
||||
$config.deployment? | default {
|
||||
mode: "docker-compose"
|
||||
@ -52,7 +52,7 @@ def get-deployment-location []: nothing -> record {
|
||||
}
|
||||
|
||||
# Critical services that must be running for provisioning to work
|
||||
def get-critical-services []: nothing -> list {
|
||||
def get-critical-services [] {
|
||||
# Get service endpoints from config
|
||||
let orchestrator_endpoint = (
|
||||
config-get "platform.orchestrator.endpoint" "http://localhost:9090/health"
|
||||
@ -93,7 +93,7 @@ def get-critical-services []: nothing -> list {
|
||||
}
|
||||
|
||||
# Check if a service is healthy
|
||||
def check-service-health [service: record]: nothing -> bool {
|
||||
def check-service-health [service: record] {
|
||||
match $service.health_check {
|
||||
"http" => {
|
||||
let result = (do {
|
||||
@ -117,7 +117,7 @@ export def bootstrap-platform [
|
||||
--force (-f) # Force restart services
|
||||
--verbose (-v) # Verbose output
|
||||
--timeout: int = 60 # Timeout in seconds
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
let critical_services = (get-critical-services)
|
||||
mut services_status = []
|
||||
@ -227,7 +227,7 @@ export def bootstrap-platform [
|
||||
def start-platform-service [
|
||||
service_name: string
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let deployment_location = (get-deployment-location)
|
||||
let deployment_mode = (get-deployment-mode)
|
||||
|
||||
@ -255,7 +255,7 @@ def start-platform-service [
|
||||
def start-service-docker-compose [
|
||||
service_name: string
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let platform_path = (config-get "platform.docker_compose.path" (get-base-path | path join "platform"))
|
||||
let compose_file = ($platform_path | path join "docker-compose.yaml")
|
||||
|
||||
@ -288,7 +288,7 @@ def start-service-docker-compose [
|
||||
def start-service-kubernetes [
|
||||
service_name: string
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let kubeconfig = (config-get "platform.kubernetes.kubeconfig" "")
|
||||
let namespace = (config-get "platform.kubernetes.namespace" "default")
|
||||
let manifests_path = (config-get "platform.kubernetes.manifests_path" (get-base-path | path join "platform" "k8s"))
|
||||
@ -359,7 +359,7 @@ def start-service-kubernetes [
|
||||
def start-service-remote-ssh [
|
||||
service_name: string
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let remote_host = (config-get "platform.remote.host" "")
|
||||
let remote_user = (config-get "platform.remote.user" "root")
|
||||
let ssh_key = (config-get "platform.remote.ssh_key" "~/.ssh/id_rsa")
|
||||
@ -401,7 +401,7 @@ def start-service-remote-ssh [
|
||||
def start-service-systemd [
|
||||
service_name: string
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
if $verbose {
|
||||
print $" Running: systemctl start ($service_name)"
|
||||
}
|
||||
@ -425,7 +425,7 @@ def wait-for-service-health [
|
||||
service: record
|
||||
--timeout: int = 60
|
||||
--verbose (-v)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
|
||||
let start_time = (date now)
|
||||
let timeout_duration = ($timeout * 1_000_000_000) # Convert to nanoseconds
|
||||
@ -467,7 +467,7 @@ def wait-for-service-health [
|
||||
# Get platform service status summary
|
||||
export def platform-status [
|
||||
--verbose (-v)
|
||||
]: nothing -> record {
|
||||
] {
|
||||
|
||||
let critical_services = (get-critical-services)
|
||||
mut status_details = []
|
||||
|
||||
@ -13,24 +13,24 @@ use ../config/accessor.nu *
|
||||
use ../commands/traits.nu *
|
||||
|
||||
# Check if auth plugin is available
|
||||
def is-plugin-available []: nothing -> bool {
|
||||
def is-plugin-available [] {
|
||||
(which auth | length) > 0
|
||||
}
|
||||
|
||||
# Check if auth plugin is enabled in config
|
||||
def is-plugin-enabled []: nothing -> bool {
|
||||
def is-plugin-enabled [] {
|
||||
config-get "plugins.auth_enabled" true
|
||||
}
|
||||
|
||||
# Get control center base URL
|
||||
def get-control-center-url []: nothing -> string {
|
||||
def get-control-center-url [] {
|
||||
config-get "platform.control_center.url" "http://localhost:3000"
|
||||
}
|
||||
|
||||
# Store token in OS keyring (requires plugin)
|
||||
def store-token-keyring [
|
||||
token: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if (is-plugin-available) {
|
||||
auth store-token $token
|
||||
} else {
|
||||
@ -39,7 +39,7 @@ def store-token-keyring [
|
||||
}
|
||||
|
||||
# Retrieve token from OS keyring (requires plugin)
|
||||
def get-token-keyring []: nothing -> string {
|
||||
def get-token-keyring [] {
|
||||
if (is-plugin-available) {
|
||||
auth get-token
|
||||
} else {
|
||||
@ -48,7 +48,7 @@ def get-token-keyring []: nothing -> string {
|
||||
}
|
||||
|
||||
# Helper to safely execute a closure and return null on error
|
||||
def try-plugin [callback: closure]: nothing -> any {
|
||||
def try-plugin [callback: closure] {
|
||||
do -i $callback
|
||||
}
|
||||
|
||||
@ -329,7 +329,7 @@ export def plugin-mfa-verify [
|
||||
}
|
||||
|
||||
# Get current authentication status
|
||||
export def plugin-auth-status []: nothing -> record {
|
||||
export def plugin-auth-status [] {
|
||||
let plugin_available = is-plugin-available
|
||||
let plugin_enabled = is-plugin-enabled
|
||||
let token = get-token-keyring
|
||||
@ -350,7 +350,7 @@ export def plugin-auth-status []: nothing -> record {
|
||||
# Get auth requirements from metadata for a specific command
|
||||
def get-metadata-auth-requirements [
|
||||
command_name: string # Command to check (e.g., "server create", "cluster delete")
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let metadata = (get-command-metadata $command_name)
|
||||
|
||||
if ($metadata | type) == "record" {
|
||||
@ -376,7 +376,7 @@ def get-metadata-auth-requirements [
|
||||
# Determine if MFA is required based on metadata auth_type
|
||||
def requires-mfa-from-metadata [
|
||||
command_name: string # Command to check
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||
$auth_reqs.auth_type == "mfa" or $auth_reqs.auth_type == "cedar"
|
||||
}
|
||||
@ -384,7 +384,7 @@ def requires-mfa-from-metadata [
|
||||
# Determine if operation is destructive based on metadata
|
||||
def is-destructive-from-metadata [
|
||||
command_name: string # Command to check
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||
$auth_reqs.side_effect_type == "delete"
|
||||
}
|
||||
@ -392,7 +392,7 @@ def is-destructive-from-metadata [
|
||||
# Check if metadata indicates this is a production operation
|
||||
def is-production-from-metadata [
|
||||
command_name: string # Command to check
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let metadata = (get-command-metadata $command_name)
|
||||
|
||||
if ($metadata | type) == "record" {
|
||||
@ -407,7 +407,7 @@ def is-production-from-metadata [
|
||||
def validate-permission-level [
|
||||
command_name: string # Command to check
|
||||
user_level: string # User's permission level (read, write, admin, superadmin)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||
let required_level = $auth_reqs.min_permission
|
||||
|
||||
@ -448,7 +448,7 @@ def validate-permission-level [
|
||||
# Determine auth enforcement based on metadata
|
||||
export def should-enforce-auth-from-metadata [
|
||||
command_name: string # Command to check
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let auth_reqs = (get-metadata-auth-requirements $command_name)
|
||||
|
||||
# If metadata explicitly requires auth, enforce it
|
||||
@ -470,7 +470,7 @@ export def should-enforce-auth-from-metadata [
|
||||
# ============================================================================
|
||||
|
||||
# Check if authentication is required based on configuration
|
||||
export def should-require-auth []: nothing -> bool {
|
||||
export def should-require-auth [] {
|
||||
let config_required = (config-get "security.require_auth" false)
|
||||
let env_bypass = ($env.PROVISIONING_SKIP_AUTH? | default "false") == "true"
|
||||
let allow_bypass = (config-get "security.bypass.allow_skip_auth" false)
|
||||
@ -479,7 +479,7 @@ export def should-require-auth []: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if MFA is required for production operations
|
||||
export def should-require-mfa-prod []: nothing -> bool {
|
||||
export def should-require-mfa-prod [] {
|
||||
let environment = (config-get "environment" "dev")
|
||||
let require_mfa = (config-get "security.require_mfa_for_production" true)
|
||||
|
||||
@ -487,24 +487,24 @@ export def should-require-mfa-prod []: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if MFA is required for destructive operations
|
||||
export def should-require-mfa-destructive []: nothing -> bool {
|
||||
export def should-require-mfa-destructive [] {
|
||||
(config-get "security.require_mfa_for_destructive" true)
|
||||
}
|
||||
|
||||
# Check if user is authenticated
|
||||
export def is-authenticated []: nothing -> bool {
|
||||
export def is-authenticated [] {
|
||||
let result = (plugin-verify)
|
||||
($result | get valid? | default false)
|
||||
}
|
||||
|
||||
# Check if MFA is verified
|
||||
export def is-mfa-verified []: nothing -> bool {
|
||||
export def is-mfa-verified [] {
|
||||
let result = (plugin-verify)
|
||||
($result | get mfa_verified? | default false)
|
||||
}
|
||||
|
||||
# Get current authenticated user
|
||||
export def get-authenticated-user []: nothing -> string {
|
||||
export def get-authenticated-user [] {
|
||||
let result = (plugin-verify)
|
||||
($result | get username? | default "")
|
||||
}
|
||||
@ -513,7 +513,7 @@ export def get-authenticated-user []: nothing -> string {
|
||||
export def require-auth [
|
||||
operation: string # Operation name for error messages
|
||||
--allow-skip # Allow skip-auth flag bypass
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Check if authentication is required
|
||||
if not (should-require-auth) {
|
||||
return true
|
||||
@ -557,7 +557,7 @@ export def require-auth [
|
||||
export def require-mfa [
|
||||
operation: string # Operation name for error messages
|
||||
reason: string # Reason MFA is required
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let auth_status = (plugin-verify)
|
||||
|
||||
if not ($auth_status | get mfa_verified? | default false) {
|
||||
@ -584,7 +584,7 @@ export def require-mfa [
|
||||
export def check-auth-for-production [
|
||||
operation: string # Operation name
|
||||
--allow-skip # Allow skip-auth flag bypass
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# First check if this command is actually production-related via metadata
|
||||
if (is-production-from-metadata $operation) {
|
||||
# Require authentication first
|
||||
@ -612,7 +612,7 @@ export def check-auth-for-production [
|
||||
export def check-auth-for-destructive [
|
||||
operation: string # Operation name
|
||||
--allow-skip # Allow skip-auth flag bypass
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Check if this is a destructive operation via metadata
|
||||
if (is-destructive-from-metadata $operation) {
|
||||
# Always require authentication for destructive ops
|
||||
@ -637,14 +637,14 @@ export def check-auth-for-destructive [
|
||||
}
|
||||
|
||||
# Helper: Check if operation is in check mode (should skip auth)
|
||||
export def is-check-mode [flags: record]: nothing -> bool {
|
||||
export def is-check-mode [flags: record] {
|
||||
(($flags | get check? | default false) or
|
||||
($flags | get check_mode? | default false) or
|
||||
($flags | get c? | default false))
|
||||
}
|
||||
|
||||
# Helper: Determine if operation is destructive
|
||||
export def is-destructive-operation [operation_type: string]: nothing -> bool {
|
||||
export def is-destructive-operation [operation_type: string] {
|
||||
$operation_type in ["delete" "destroy" "remove"]
|
||||
}
|
||||
|
||||
@ -653,7 +653,7 @@ export def check-operation-auth [
|
||||
operation_name: string # Name of operation
|
||||
operation_type: string # Type: create, delete, modify, read
|
||||
flags?: record # Command flags
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Skip in check mode
|
||||
if ($flags | is-not-empty) and (is-check-mode $flags) {
|
||||
print $"(ansi dim)Skipping authentication check (check mode)(ansi reset)"
|
||||
@ -712,7 +712,7 @@ export def check-operation-auth [
|
||||
}
|
||||
|
||||
# Get authentication metadata for audit logging
|
||||
export def get-auth-metadata []: nothing -> record {
|
||||
export def get-auth-metadata [] {
|
||||
let auth_status = (plugin-verify)
|
||||
|
||||
{
|
||||
@ -727,7 +727,7 @@ export def get-auth-metadata []: nothing -> record {
|
||||
export def log-authenticated-operation [
|
||||
operation: string # Operation performed
|
||||
details: record # Operation details
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let auth_metadata = (get-auth-metadata)
|
||||
|
||||
let log_entry = {
|
||||
@ -749,7 +749,7 @@ export def log-authenticated-operation [
|
||||
}
|
||||
|
||||
# Print current authentication status (user-friendly)
|
||||
export def print-auth-status []: nothing -> nothing {
|
||||
export def print-auth-status [] {
|
||||
let auth_status = (plugin-verify)
|
||||
let is_valid = ($auth_status | get valid? | default false)
|
||||
|
||||
@ -788,7 +788,7 @@ export def print-auth-status []: nothing -> nothing {
|
||||
def run-typedialog-auth-form [
|
||||
wrapper_script: string
|
||||
--backend: string = "tui"
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Check if the wrapper script exists
|
||||
if not ($wrapper_script | path exists) {
|
||||
return {
|
||||
@ -824,20 +824,23 @@ def run-typedialog-auth-form [
|
||||
}
|
||||
|
||||
# Parse JSON output
|
||||
let values = (try {
|
||||
let result = do {
|
||||
open $json_output | from json
|
||||
} catch {
|
||||
} | complete
|
||||
|
||||
if $result.exit_code == 0 {
|
||||
let values = $result.stdout
|
||||
{
|
||||
success: true
|
||||
values: $values
|
||||
use_fallback: false
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false
|
||||
error: "Failed to parse TypeDialog output"
|
||||
use_fallback: true
|
||||
}
|
||||
})
|
||||
|
||||
{
|
||||
success: true
|
||||
values: $values
|
||||
use_fallback: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,27 +4,27 @@
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# Check if KMS plugin is available
|
||||
def is-plugin-available []: nothing -> bool {
|
||||
def is-plugin-available [] {
|
||||
(which kms | length) > 0
|
||||
}
|
||||
|
||||
# Check if KMS plugin is enabled in config
|
||||
def is-plugin-enabled []: nothing -> bool {
|
||||
def is-plugin-enabled [] {
|
||||
config-get "plugins.kms_enabled" true
|
||||
}
|
||||
|
||||
# Get KMS service base URL
|
||||
def get-kms-url []: nothing -> string {
|
||||
def get-kms-url [] {
|
||||
config-get "platform.kms_service.url" "http://localhost:8090"
|
||||
}
|
||||
|
||||
# Get default KMS backend
|
||||
def get-default-backend []: nothing -> string {
|
||||
def get-default-backend [] {
|
||||
config-get "security.kms.backend" "rustyvault"
|
||||
}
|
||||
|
||||
# Helper to safely execute a closure and return null on error
|
||||
def try-plugin [callback: closure]: nothing -> any {
|
||||
def try-plugin [callback: closure] {
|
||||
do -i $callback
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ export def plugin-kms-generate-key [
|
||||
}
|
||||
|
||||
# Get KMS service status
|
||||
export def plugin-kms-status []: nothing -> record {
|
||||
export def plugin-kms-status [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -236,7 +236,7 @@ export def plugin-kms-status []: nothing -> record {
|
||||
}
|
||||
|
||||
# List available KMS backends
|
||||
export def plugin-kms-backends []: nothing -> table {
|
||||
export def plugin-kms-backends [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -324,7 +324,7 @@ export def plugin-kms-rotate-key [
|
||||
# List encryption keys
|
||||
export def plugin-kms-list-keys [
|
||||
--backend: string = "" # rustyvault, age, vault, cosmian, aws-kms
|
||||
]: nothing -> table {
|
||||
] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
let backend_name = if ($backend | is-empty) { get-default-backend } else { $backend }
|
||||
@ -360,7 +360,7 @@ export def plugin-kms-list-keys [
|
||||
}
|
||||
|
||||
# Get KMS plugin status and configuration
|
||||
export def plugin-kms-info []: nothing -> record {
|
||||
export def plugin-kms-info [] {
|
||||
let plugin_available = is-plugin-available
|
||||
let plugin_enabled = is-plugin-enabled
|
||||
let default_backend = get-default-backend
|
||||
|
||||
@ -269,15 +269,15 @@ export def test_file_encryption [] {
|
||||
let test_file = "/tmp/kms_test_file.txt"
|
||||
let test_content = "This is test file content for KMS encryption"
|
||||
|
||||
let result = (do {
|
||||
try {
|
||||
$test_content | save -f $test_file
|
||||
|
||||
# Try to encrypt file
|
||||
let encrypt_result = (do {
|
||||
let result = (do {
|
||||
plugin-kms-encrypt-file $test_file "age"
|
||||
} | complete)
|
||||
|
||||
if $encrypt_result.exit_code == 0 {
|
||||
if $result.exit_code == 0 {
|
||||
print " ✅ File encryption succeeded"
|
||||
|
||||
# Cleanup
|
||||
@ -286,9 +286,7 @@ export def test_file_encryption [] {
|
||||
} else {
|
||||
print " ⚠️ File encryption not available"
|
||||
}
|
||||
} | complete)
|
||||
|
||||
if $result.exit_code != 0 {
|
||||
} catch { |err|
|
||||
print " ⚠️ Could not create test file"
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export use secretumvault.nu *
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# List all available plugins with status
|
||||
export def list-plugins []: nothing -> table {
|
||||
export def list-plugins [] {
|
||||
let installed_str = (version).installed_plugins
|
||||
let installed_list = ($installed_str | split row ", ")
|
||||
|
||||
@ -77,7 +77,7 @@ export def list-plugins []: nothing -> table {
|
||||
# Register a plugin with Nushell
|
||||
export def register-plugin [
|
||||
plugin_name: string # Name of plugin binary (e.g., nu_plugin_auth)
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let plugin_path = (which $plugin_name | get path.0?)
|
||||
|
||||
if ($plugin_path | is-empty) {
|
||||
@ -113,7 +113,7 @@ export def register-plugin [
|
||||
# Test plugin functionality
|
||||
export def test-plugin [
|
||||
plugin_name: string # auth, kms, secretumvault, tera, nickel
|
||||
]: nothing -> record {
|
||||
] {
|
||||
match $plugin_name {
|
||||
"auth" => {
|
||||
print $"(_ansi cyan)Testing auth plugin...(_ansi reset)"
|
||||
@ -170,7 +170,7 @@ export def test-plugin [
|
||||
}
|
||||
|
||||
# Get plugin build information
|
||||
export def plugin-build-info []: nothing -> record {
|
||||
export def plugin-build-info [] {
|
||||
let plugin_dir = ($env.PWD | path join "_nushell-plugins")
|
||||
|
||||
if not ($plugin_dir | path exists) {
|
||||
@ -193,7 +193,7 @@ export def plugin-build-info []: nothing -> record {
|
||||
# Build plugins from source
|
||||
export def build-plugins [
|
||||
--plugin: string = "" # Specific plugin to build (empty = all)
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
let plugin_dir = ($env.PWD | path join "_nushell-plugins")
|
||||
|
||||
if not ($plugin_dir | path exists) {
|
||||
|
||||
@ -4,33 +4,33 @@
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# Check if orchestrator plugin is available
|
||||
def is-plugin-available []: nothing -> bool {
|
||||
def is-plugin-available [] {
|
||||
(which orch | length) > 0
|
||||
}
|
||||
|
||||
# Check if orchestrator plugin is enabled in config
|
||||
def is-plugin-enabled []: nothing -> bool {
|
||||
def is-plugin-enabled [] {
|
||||
config-get "plugins.orchestrator_enabled" true
|
||||
}
|
||||
|
||||
# Get orchestrator base URL
|
||||
def get-orchestrator-url []: nothing -> string {
|
||||
def get-orchestrator-url [] {
|
||||
config-get "platform.orchestrator.url" "http://localhost:8080"
|
||||
}
|
||||
|
||||
# Get orchestrator data directory
|
||||
def get-orchestrator-data-dir []: nothing -> path {
|
||||
def get-orchestrator-data-dir [] {
|
||||
let base = config-get "paths.base" $env.PWD
|
||||
$"($base)/provisioning/platform/orchestrator/data"
|
||||
}
|
||||
|
||||
# Helper to safely execute a closure and return null on error
|
||||
def try-plugin [callback: closure]: nothing -> any {
|
||||
def try-plugin [callback: closure] {
|
||||
do -i $callback
|
||||
}
|
||||
|
||||
# Get orchestrator status (fastest: direct file access)
|
||||
export def plugin-orch-status []: nothing -> record {
|
||||
export def plugin-orch-status [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -92,7 +92,7 @@ export def plugin-orch-status []: nothing -> record {
|
||||
export def plugin-orch-tasks [
|
||||
--status: string = "" # pending, running, completed, failed
|
||||
--limit: int = 100 # Maximum number of tasks
|
||||
]: nothing -> table {
|
||||
] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -174,7 +174,7 @@ export def plugin-orch-tasks [
|
||||
# Get specific task details
|
||||
export def plugin-orch-task [
|
||||
task_id: string
|
||||
]: nothing -> any {
|
||||
] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -235,7 +235,7 @@ export def plugin-orch-task [
|
||||
}
|
||||
|
||||
# Validate orchestrator configuration
|
||||
export def plugin-orch-validate []: nothing -> record {
|
||||
export def plugin-orch-validate [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -268,7 +268,7 @@ export def plugin-orch-validate []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get orchestrator statistics
|
||||
export def plugin-orch-stats []: nothing -> record {
|
||||
export def plugin-orch-stats [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -353,7 +353,7 @@ export def plugin-orch-stats []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get orchestrator plugin information
|
||||
export def plugin-orch-info []: nothing -> record {
|
||||
export def plugin-orch-info [] {
|
||||
let plugin_available = is-plugin-available
|
||||
let plugin_enabled = is-plugin-enabled
|
||||
let orchestrator_url = get-orchestrator-url
|
||||
|
||||
@ -4,22 +4,22 @@
|
||||
use ../config/accessor.nu *
|
||||
|
||||
# Check if SecretumVault plugin is available
|
||||
def is-plugin-available []: nothing -> bool {
|
||||
def is-plugin-available [] {
|
||||
(which secretumvault | length) > 0
|
||||
}
|
||||
|
||||
# Check if SecretumVault plugin is enabled in config
|
||||
def is-plugin-enabled []: nothing -> bool {
|
||||
def is-plugin-enabled [] {
|
||||
config-get "plugins.secretumvault_enabled" true
|
||||
}
|
||||
|
||||
# Get SecretumVault service URL
|
||||
def get-secretumvault-url []: nothing -> string {
|
||||
def get-secretumvault-url [] {
|
||||
config-get "kms.secretumvault.server_url" "http://localhost:8200"
|
||||
}
|
||||
|
||||
# Get SecretumVault auth token
|
||||
def get-secretumvault-token []: nothing -> string {
|
||||
def get-secretumvault-token [] {
|
||||
let token = (
|
||||
if ($env.SECRETUMVAULT_TOKEN? != null) {
|
||||
$env.SECRETUMVAULT_TOKEN
|
||||
@ -35,17 +35,17 @@ def get-secretumvault-token []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get SecretumVault mount point
|
||||
def get-secretumvault-mount-point []: nothing -> string {
|
||||
def get-secretumvault-mount-point [] {
|
||||
config-get "kms.secretumvault.mount_point" "transit"
|
||||
}
|
||||
|
||||
# Get default SecretumVault key name
|
||||
def get-secretumvault-key-name []: nothing -> string {
|
||||
def get-secretumvault-key-name [] {
|
||||
config-get "kms.secretumvault.key_name" "provisioning-master"
|
||||
}
|
||||
|
||||
# Helper to safely execute a closure and return null on error
|
||||
def try-plugin [callback: closure]: nothing -> any {
|
||||
def try-plugin [callback: closure] {
|
||||
do -i $callback
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ export def plugin-secretumvault-generate-key [
|
||||
}
|
||||
|
||||
# Check SecretumVault health using plugin
|
||||
export def plugin-secretumvault-health []: nothing -> record {
|
||||
export def plugin-secretumvault-health [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -287,7 +287,7 @@ export def plugin-secretumvault-health []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get SecretumVault version using plugin
|
||||
export def plugin-secretumvault-version []: nothing -> string {
|
||||
export def plugin-secretumvault-version [] {
|
||||
let enabled = is-plugin-enabled
|
||||
let available = is-plugin-available
|
||||
|
||||
@ -383,7 +383,7 @@ export def plugin-secretumvault-rotate-key [
|
||||
}
|
||||
|
||||
# Get SecretumVault plugin status and configuration
|
||||
export def plugin-secretumvault-info []: nothing -> record {
|
||||
export def plugin-secretumvault-info [] {
|
||||
let plugin_available = is-plugin-available
|
||||
let plugin_enabled = is-plugin-enabled
|
||||
let sv_url = get-secretumvault-url
|
||||
|
||||
@ -4,7 +4,7 @@ use config/accessor.nu *
|
||||
export def clip_copy [
|
||||
msg: string
|
||||
show: bool
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if ( (version).installed_plugins | str contains "clipboard" ) {
|
||||
$msg | clipboard copy
|
||||
print $"(_ansi default_dimmed)copied into clipboard now (_ansi reset)"
|
||||
@ -20,7 +20,7 @@ export def notify_msg [
|
||||
time_body: string
|
||||
timeout: duration
|
||||
task?: closure
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
if ( (version).installed_plugins | str contains "desktop_notifications" ) {
|
||||
if $task != null {
|
||||
( notify -s $title -t $time_body --timeout $timeout -i $icon)
|
||||
@ -42,7 +42,7 @@ export def notify_msg [
|
||||
|
||||
export def show_qr [
|
||||
url: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
# Try to use pre-generated QR code files
|
||||
let qr_path = ((get-provisioning-resources) | path join "qrs" | path join ($url | path basename))
|
||||
if ($qr_path | path exists) {
|
||||
@ -58,7 +58,7 @@ export def port_scan [
|
||||
ip: string
|
||||
port: int
|
||||
sec_timeout: int
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Use netcat for port scanning - reliable and portable
|
||||
(^nc -zv -w $sec_timeout ($ip | str trim) $port err> (if $nu.os-info.name == "windows" { "NUL" } else { "/dev/null" }) | complete).exit_code == 0
|
||||
}
|
||||
@ -67,7 +67,7 @@ export def render_template [
|
||||
template_path: string
|
||||
vars: record
|
||||
--ai_prompt: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Regular template rendering
|
||||
if ( (version).installed_plugins | str contains "tera" ) {
|
||||
$vars | tera-render $template_path
|
||||
@ -79,7 +79,7 @@ export def render_template [
|
||||
export def render_template_ai [
|
||||
ai_prompt: string
|
||||
template_type: string = "template"
|
||||
]: nothing -> string {
|
||||
] {
|
||||
use ai/lib.nu *
|
||||
ai_generate_template $ai_prompt $template_type
|
||||
}
|
||||
@ -87,7 +87,7 @@ export def render_template_ai [
|
||||
export def process_decl_file [
|
||||
decl_file: string
|
||||
format: string
|
||||
]: nothing -> string {
|
||||
] {
|
||||
# Use external Nickel CLI (nickel export)
|
||||
if (get-use-nickel) {
|
||||
let result = (^nickel export $decl_file --format $format | complete)
|
||||
@ -104,7 +104,7 @@ export def process_decl_file [
|
||||
export def validate_decl_schema [
|
||||
decl_file: string
|
||||
data: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Validate using external Nickel CLI
|
||||
if (get-use-nickel) {
|
||||
let data_json = ($data | to json)
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
# metadata for audit logging purposes.
|
||||
|
||||
# Standard provider interface - all providers must implement these functions
|
||||
export def get-provider-interface []: nothing -> record {
|
||||
export def get-provider-interface [] {
|
||||
{
|
||||
# Server query operations
|
||||
query_servers: {
|
||||
@ -145,7 +145,7 @@ export def get-provider-interface []: nothing -> record {
|
||||
export def validate-provider-interface [
|
||||
provider_name: string
|
||||
provider_module: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let interface = (get-provider-interface)
|
||||
let required_functions = ($interface | columns)
|
||||
|
||||
@ -178,7 +178,7 @@ export def validate-provider-interface [
|
||||
}
|
||||
|
||||
# Get provider interface documentation
|
||||
export def get-provider-interface-docs []: nothing -> table {
|
||||
export def get-provider-interface-docs [] {
|
||||
let interface = (get-provider-interface)
|
||||
|
||||
$interface | transpose function details | each {|row|
|
||||
@ -191,7 +191,7 @@ export def get-provider-interface-docs []: nothing -> table {
|
||||
}
|
||||
|
||||
# Provider capability flags - optional extensions
|
||||
export def get-provider-capabilities []: nothing -> record {
|
||||
export def get-provider-capabilities [] {
|
||||
{
|
||||
# Core capabilities (required for all providers)
|
||||
server_management: true
|
||||
@ -223,7 +223,7 @@ export def get-provider-capabilities []: nothing -> record {
|
||||
}
|
||||
|
||||
# Provider interface version
|
||||
export def get-interface-version []: nothing -> string {
|
||||
export def get-interface-version [] {
|
||||
"1.0.0"
|
||||
}
|
||||
|
||||
@ -272,7 +272,7 @@ export def get-interface-version []: nothing -> string {
|
||||
# server: record
|
||||
# check: bool
|
||||
# wait: bool
|
||||
# ]: nothing -> bool {
|
||||
# ] {
|
||||
# # Log the operation with user context
|
||||
# let auth_metadata = (get-auth-metadata)
|
||||
# log-authenticated-operation "aws_create_server" {
|
||||
|
||||
@ -6,7 +6,7 @@ use interface.nu *
|
||||
use ../utils/logging.nu *
|
||||
|
||||
# Load provider dynamically with validation
|
||||
export def load-provider [name: string]: nothing -> record {
|
||||
export def load-provider [name: string] {
|
||||
# Silent loading - only log errors, not info/success
|
||||
# Provider loading happens multiple times due to wrapper scripts, logging creates noise
|
||||
|
||||
@ -43,7 +43,7 @@ export def load-provider [name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Load core provider
|
||||
def load-core-provider [provider_entry: record]: nothing -> record {
|
||||
def load-core-provider [provider_entry: record] {
|
||||
# For core providers, use direct module loading
|
||||
# Core providers should be in the core library path
|
||||
let module_path = $provider_entry.entry_point
|
||||
@ -59,7 +59,7 @@ def load-core-provider [provider_entry: record]: nothing -> record {
|
||||
}
|
||||
|
||||
# Load extension provider
|
||||
def load-extension-provider [provider_entry: record]: nothing -> record {
|
||||
def load-extension-provider [provider_entry: record] {
|
||||
# For extension providers, use the adapter pattern
|
||||
let module_path = $provider_entry.entry_point
|
||||
|
||||
@ -84,7 +84,7 @@ def load-extension-provider [provider_entry: record]: nothing -> record {
|
||||
}
|
||||
|
||||
# Get provider instance (with caching)
|
||||
export def get-provider [name: string]: nothing -> record {
|
||||
export def get-provider [name: string] {
|
||||
# Check if already loaded in this session
|
||||
let cache_key = $"PROVIDER_LOADED_($name)"
|
||||
let cached_value = if ($cache_key in ($env | columns)) { $env | get $cache_key } else { null }
|
||||
@ -105,7 +105,7 @@ export def call-provider-function [
|
||||
provider_name: string
|
||||
function_name: string
|
||||
...args
|
||||
]: nothing -> any {
|
||||
] {
|
||||
# Get provider entry
|
||||
let provider_entry = (get-provider-entry $provider_name)
|
||||
|
||||
@ -185,7 +185,7 @@ let args = \(open ($args_file)\)
|
||||
}
|
||||
|
||||
# Get required provider functions
|
||||
def get-required-functions []: nothing -> list<string> {
|
||||
def get-required-functions [] {
|
||||
[
|
||||
"get-provider-metadata"
|
||||
"query_servers"
|
||||
@ -195,7 +195,7 @@ def get-required-functions []: nothing -> list<string> {
|
||||
}
|
||||
|
||||
# Validate provider interface compliance
|
||||
def validate-provider-interface [provider_name: string, provider_instance: record]: nothing -> record {
|
||||
def validate-provider-interface [provider_name: string, provider_instance: record] {
|
||||
let required_functions = (get-required-functions)
|
||||
mut missing_functions = []
|
||||
mut valid = true
|
||||
@ -237,7 +237,7 @@ def validate-provider-interface [provider_name: string, provider_instance: recor
|
||||
}
|
||||
|
||||
# Load multiple providers
|
||||
export def load-providers [provider_names: list<string>]: nothing -> record {
|
||||
export def load-providers [provider_names: list<string>] {
|
||||
mut results = {
|
||||
successful: 0
|
||||
failed: 0
|
||||
@ -268,7 +268,7 @@ export def load-providers [provider_names: list<string>]: nothing -> record {
|
||||
}
|
||||
|
||||
# Check provider health
|
||||
export def check-provider-health [provider_name: string]: nothing -> record {
|
||||
export def check-provider-health [provider_name: string] {
|
||||
let health_check = {
|
||||
provider: $provider_name
|
||||
available: false
|
||||
@ -309,7 +309,7 @@ export def check-provider-health [provider_name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Check health of all providers
|
||||
export def check-all-providers-health []: nothing -> table {
|
||||
export def check-all-providers-health [] {
|
||||
let providers = (list-providers --available-only)
|
||||
|
||||
$providers | each {|provider|
|
||||
@ -318,7 +318,7 @@ export def check-all-providers-health []: nothing -> table {
|
||||
}
|
||||
|
||||
# Get loader statistics
|
||||
export def get-loader-stats []: nothing -> record {
|
||||
export def get-loader-stats [] {
|
||||
let provider_stats = (get-provider-stats)
|
||||
let health_checks = (check-all-providers-health)
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ use ../utils/logging.nu *
|
||||
use interface.nu *
|
||||
|
||||
# Provider registry cache file path
|
||||
def get-provider-cache-file []: nothing -> string {
|
||||
def get-provider-cache-file [] {
|
||||
let cache_dir = ($env.HOME | path join ".cache" "provisioning")
|
||||
if not ($cache_dir | path exists) {
|
||||
mkdir $cache_dir
|
||||
@ -15,17 +15,17 @@ def get-provider-cache-file []: nothing -> string {
|
||||
}
|
||||
|
||||
# Check if registry is initialized
|
||||
def is-registry-initialized []: nothing -> bool {
|
||||
def is-registry-initialized [] {
|
||||
($env.PROVIDER_REGISTRY_INITIALIZED? | default false)
|
||||
}
|
||||
|
||||
# Mark registry as initialized
|
||||
def mark-registry-initialized []: nothing -> nothing {
|
||||
def mark-registry-initialized [] {
|
||||
$env.PROVIDER_REGISTRY_INITIALIZED = true
|
||||
}
|
||||
|
||||
# Initialize the provider registry
|
||||
export def init-provider-registry []: nothing -> nothing {
|
||||
export def init-provider-registry [] {
|
||||
if (is-registry-initialized) {
|
||||
return
|
||||
}
|
||||
@ -49,7 +49,7 @@ export def init-provider-registry []: nothing -> nothing {
|
||||
}
|
||||
|
||||
# Get provider registry from cache or discover
|
||||
def get-provider-registry []: nothing -> record {
|
||||
def get-provider-registry [] {
|
||||
let cache_file = (get-provider-cache-file)
|
||||
if ($cache_file | path exists) {
|
||||
open $cache_file
|
||||
@ -59,7 +59,7 @@ def get-provider-registry []: nothing -> record {
|
||||
}
|
||||
|
||||
# Discover providers without full registration
|
||||
def discover-providers-only []: nothing -> record {
|
||||
def discover-providers-only [] {
|
||||
mut registry = {}
|
||||
|
||||
# Get provisioning system path from config or environment
|
||||
@ -103,7 +103,7 @@ def discover-providers-only []: nothing -> record {
|
||||
}
|
||||
|
||||
# Discover and register all providers
|
||||
def discover-and-register-providers []: nothing -> nothing {
|
||||
def discover-and-register-providers [] {
|
||||
let registry = (discover-providers-only)
|
||||
|
||||
# Save to cache
|
||||
@ -114,7 +114,7 @@ def discover-and-register-providers []: nothing -> nothing {
|
||||
}
|
||||
|
||||
# Discover providers in a specific directory
|
||||
def discover-providers-in-directory [base_path: string, provider_type: string]: nothing -> record {
|
||||
def discover-providers-in-directory [base_path: string, provider_type: string] {
|
||||
mut providers = {}
|
||||
|
||||
if not ($base_path | path exists) {
|
||||
@ -164,7 +164,7 @@ def discover-providers-in-directory [base_path: string, provider_type: string]:
|
||||
export def list-providers [
|
||||
--available-only # Only show available providers
|
||||
--verbose # Show detailed information
|
||||
]: nothing -> table {
|
||||
] {
|
||||
if not (is-registry-initialized) {
|
||||
init-provider-registry | ignore
|
||||
}
|
||||
@ -186,7 +186,7 @@ export def list-providers [
|
||||
}
|
||||
|
||||
# Check if a provider is available
|
||||
export def is-provider-available [provider_name: string]: nothing -> bool {
|
||||
export def is-provider-available [provider_name: string] {
|
||||
if not (is-registry-initialized) {
|
||||
init-provider-registry | ignore
|
||||
}
|
||||
@ -202,7 +202,7 @@ export def is-provider-available [provider_name: string]: nothing -> bool {
|
||||
}
|
||||
|
||||
# Get provider entry information
|
||||
export def get-provider-entry [provider_name: string]: nothing -> record {
|
||||
export def get-provider-entry [provider_name: string] {
|
||||
if not (is-registry-initialized) {
|
||||
init-provider-registry | ignore
|
||||
}
|
||||
@ -217,7 +217,7 @@ export def get-provider-entry [provider_name: string]: nothing -> record {
|
||||
}
|
||||
|
||||
# Get provider registry statistics
|
||||
export def get-provider-stats []: nothing -> record {
|
||||
export def get-provider-stats [] {
|
||||
if not (is-registry-initialized) {
|
||||
init-provider-registry | ignore
|
||||
}
|
||||
@ -235,7 +235,7 @@ export def get-provider-stats []: nothing -> record {
|
||||
}
|
||||
|
||||
# Get capabilities for a specific provider
|
||||
export def get-provider-capabilities-for [provider_name: string]: nothing -> record {
|
||||
export def get-provider-capabilities-for [provider_name: string] {
|
||||
if not (is-provider-available $provider_name) {
|
||||
return {}
|
||||
}
|
||||
@ -254,7 +254,7 @@ export def get-provider-capabilities-for [provider_name: string]: nothing -> rec
|
||||
}
|
||||
|
||||
# Refresh the provider registry
|
||||
export def refresh-provider-registry []: nothing -> nothing {
|
||||
export def refresh-provider-registry [] {
|
||||
# Clear cache
|
||||
let cache_file = (get-provider-cache-file)
|
||||
if ($cache_file | path exists) {
|
||||
|
||||
@ -180,7 +180,7 @@ export def "platform health" [] {
|
||||
print "Platform Health Check\n"
|
||||
|
||||
# Helper to check health status recursively
|
||||
def check-health-status [services: list, healthy: int, unhealthy: int, unknown: int]: nothing -> record {
|
||||
def check-health-status [services: list, healthy: int, unhealthy: int, unknown: int] {
|
||||
if ($services | is-empty) {
|
||||
return { healthy: $healthy, unhealthy: $unhealthy, unknown: $unknown }
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ use manager.nu [load-service-registry get-service-definition]
|
||||
# Resolve service dependencies
|
||||
export def resolve-dependencies [
|
||||
service_name: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
if ($service_def.dependencies | is-empty) {
|
||||
@ -16,7 +16,7 @@ export def resolve-dependencies [
|
||||
}
|
||||
|
||||
# Recursively resolve dependencies - collect all unique deps
|
||||
def accumulate-deps [deps: list, all_deps: list]: nothing -> list {
|
||||
def accumulate-deps [deps: list, all_deps: list] {
|
||||
if ($deps | is-empty) {
|
||||
return $all_deps
|
||||
}
|
||||
@ -36,7 +36,7 @@ export def resolve-dependencies [
|
||||
# Get dependency tree
|
||||
export def get-dependency-tree [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
if ($service_def.dependencies | is-empty) {
|
||||
@ -63,7 +63,7 @@ export def get-dependency-tree [
|
||||
def topological-sort [
|
||||
services: list
|
||||
dep_map: record
|
||||
]: nothing -> list {
|
||||
] {
|
||||
# Recursive DFS helper function
|
||||
def visit [
|
||||
node: string
|
||||
@ -71,7 +71,7 @@ def topological-sort [
|
||||
visited: record
|
||||
visiting: record
|
||||
sorted: list
|
||||
]: nothing -> record {
|
||||
] {
|
||||
if $node in ($visiting | columns) {
|
||||
error make {
|
||||
msg: "Circular dependency detected"
|
||||
@ -95,7 +95,7 @@ def topological-sort [
|
||||
}
|
||||
|
||||
# Process dependencies recursively
|
||||
def visit-deps [deps: list, state: record]: nothing -> record {
|
||||
def visit-deps [deps: list, state: record] {
|
||||
if ($deps | is-empty) {
|
||||
return $state
|
||||
}
|
||||
@ -115,7 +115,7 @@ def topological-sort [
|
||||
}
|
||||
|
||||
# Visit all nodes recursively starting with empty state
|
||||
def visit-services [services: list, state: record]: nothing -> record {
|
||||
def visit-services [services: list, state: record] {
|
||||
if ($services | is-empty) {
|
||||
return $state
|
||||
}
|
||||
@ -135,12 +135,12 @@ def topological-sort [
|
||||
# Start services in dependency order
|
||||
export def start-services-with-deps [
|
||||
service_names: list
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Build dependency map
|
||||
let registry = (load-service-registry)
|
||||
|
||||
# Helper to build dep_map from registry entries
|
||||
def build-dep-map [entries: list, acc: record]: nothing -> record {
|
||||
def build-dep-map [entries: list, acc: record] {
|
||||
if ($entries | is-empty) {
|
||||
return $acc
|
||||
}
|
||||
@ -153,7 +153,7 @@ export def start-services-with-deps [
|
||||
let dep_map = (build-dep-map ($registry | transpose name config) {})
|
||||
|
||||
# Helper to collect all services with their dependencies
|
||||
def collect-services [services: list, all_deps: list]: nothing -> list {
|
||||
def collect-services [services: list, all_deps: list] {
|
||||
if ($services | is-empty) {
|
||||
return $all_deps
|
||||
}
|
||||
@ -172,7 +172,7 @@ export def start-services-with-deps [
|
||||
print $"Starting services in order: ($startup_order | str join ' -> ')"
|
||||
|
||||
# Helper to start services recursively
|
||||
def start-services [services: list, state: record]: nothing -> record {
|
||||
def start-services [services: list, state: record] {
|
||||
if ($services | is-empty) {
|
||||
return $state
|
||||
}
|
||||
@ -228,11 +228,11 @@ export def start-services-with-deps [
|
||||
}
|
||||
|
||||
# Validate dependency graph (detect cycles)
|
||||
export def validate-dependency-graph []: nothing -> record {
|
||||
export def validate-dependency-graph [] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
# Helper to build dep_map from registry entries
|
||||
def build-dep-map [entries: list, acc: record]: nothing -> record {
|
||||
def build-dep-map [entries: list, acc: record] {
|
||||
if ($entries | is-empty) {
|
||||
return $acc
|
||||
}
|
||||
@ -271,11 +271,11 @@ export def validate-dependency-graph []: nothing -> record {
|
||||
# Get startup order
|
||||
export def get-startup-order [
|
||||
service_names: list
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
# Helper to build dep_map from registry entries
|
||||
def build-dep-map [entries: list, acc: record]: nothing -> record {
|
||||
def build-dep-map [entries: list, acc: record] {
|
||||
if ($entries | is-empty) {
|
||||
return $acc
|
||||
}
|
||||
@ -288,7 +288,7 @@ export def get-startup-order [
|
||||
let dep_map = (build-dep-map ($registry | transpose name config) {})
|
||||
|
||||
# Helper to collect all services with their dependencies
|
||||
def collect-services [services: list, all_deps: list]: nothing -> list {
|
||||
def collect-services [services: list, all_deps: list] {
|
||||
if ($services | is-empty) {
|
||||
return $all_deps
|
||||
}
|
||||
@ -332,7 +332,7 @@ export def get-startup-order [
|
||||
# Get reverse dependencies (which services depend on this one)
|
||||
export def get-reverse-dependencies [
|
||||
service_name: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
$registry
|
||||
@ -344,11 +344,11 @@ export def get-reverse-dependencies [
|
||||
}
|
||||
|
||||
# Get dependency graph visualization
|
||||
export def visualize-dependency-graph []: nothing -> string {
|
||||
export def visualize-dependency-graph [] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
# Helper to format a single service's dependencies
|
||||
def format-service-deps [service: string, lines: list]: nothing -> list {
|
||||
def format-service-deps [service: string, lines: list] {
|
||||
let service_def = (get-service-definition $service)
|
||||
|
||||
let base_lines = (
|
||||
@ -399,7 +399,7 @@ export def visualize-dependency-graph []: nothing -> string {
|
||||
}
|
||||
|
||||
# Helper to format all services recursively
|
||||
def format-services [services: list, lines: list]: nothing -> list {
|
||||
def format-services [services: list, lines: list] {
|
||||
if ($services | is-empty) {
|
||||
return $lines
|
||||
}
|
||||
@ -420,7 +420,7 @@ export def visualize-dependency-graph []: nothing -> string {
|
||||
# Check if service can be stopped safely
|
||||
export def can-stop-service [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
use manager.nu is-service-running
|
||||
|
||||
let reverse_deps = (get-reverse-dependencies $service_name)
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
export def perform-health-check [
|
||||
service_name: string
|
||||
health_config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let start_time = (date now)
|
||||
|
||||
let result = match $health_config.type {
|
||||
@ -47,7 +47,7 @@ export def perform-health-check [
|
||||
# HTTP health check
|
||||
def http-health-check [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let timeout = $config.timeout? | default 5
|
||||
|
||||
let http_result = (do {
|
||||
@ -81,7 +81,7 @@ def http-health-check [
|
||||
# TCP health check
|
||||
def tcp-health-check [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let timeout = $config.timeout? | default 5
|
||||
|
||||
let result = (do {
|
||||
@ -99,7 +99,7 @@ def tcp-health-check [
|
||||
# Command health check
|
||||
def command-health-check [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let result = (do {
|
||||
bash -c $config.command
|
||||
} | complete)
|
||||
@ -117,7 +117,7 @@ def command-health-check [
|
||||
# File health check
|
||||
def file-health-check [
|
||||
config: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let path_exists = ($config.path | path exists)
|
||||
|
||||
if $config.must_exist {
|
||||
@ -139,7 +139,7 @@ def file-health-check [
|
||||
export def retry-health-check [
|
||||
service_name: string
|
||||
health_config: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let max_retries = $health_config.retries? | default 3
|
||||
let interval = $health_config.interval? | default 10
|
||||
|
||||
@ -165,7 +165,7 @@ export def wait-for-service [
|
||||
service_name: string
|
||||
timeout: int
|
||||
health_config?: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# If health_config not provided, use default health check config
|
||||
let health_check = $health_config | default {
|
||||
type: "http"
|
||||
@ -183,7 +183,7 @@ export def wait-for-service [
|
||||
let timeout_ns = ($timeout * 1_000_000_000) # Convert to nanoseconds
|
||||
|
||||
# Define recursive wait function
|
||||
def wait_loop [service: string, config: record, start: any, timeout_ns: int, interval: int]: nothing -> bool {
|
||||
def wait_loop [service: string, config: record, start: any, timeout_ns: int, interval: int] {
|
||||
let check_result = (perform-health-check $service $config)
|
||||
|
||||
if $check_result.healthy {
|
||||
@ -212,7 +212,7 @@ export def get-health-status [
|
||||
service_name: string
|
||||
is_running: bool = false
|
||||
health_config?: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Parameters avoid circular dependency with manager.nu
|
||||
# If is_running is false, return stopped status
|
||||
if not $is_running {
|
||||
|
||||
@ -3,11 +3,11 @@
|
||||
# Service Lifecycle Management
|
||||
# Handles starting and stopping services based on deployment mode
|
||||
|
||||
def get-service-pid-dir []: nothing -> string {
|
||||
def get-service-pid-dir [] {
|
||||
$"($env.HOME)/.provisioning/services/pids"
|
||||
}
|
||||
|
||||
def get-service-log-dir []: nothing -> string {
|
||||
def get-service-log-dir [] {
|
||||
$"($env.HOME)/.provisioning/services/logs"
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ def get-service-log-dir []: nothing -> string {
|
||||
export def start-service-by-mode [
|
||||
service_def: record
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
match $service_def.deployment.mode {
|
||||
"binary" => {
|
||||
start-binary-service $service_def $service_name
|
||||
@ -45,7 +45,7 @@ export def start-service-by-mode [
|
||||
def start-binary-service [
|
||||
service_def: record
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let binary_config = $service_def.deployment.binary
|
||||
let binary_path = ($binary_config.binary_path | str replace -a '${HOME}' $env.HOME)
|
||||
|
||||
@ -118,7 +118,7 @@ def start-binary-service [
|
||||
def start-docker-service [
|
||||
service_def: record
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let docker_config = $service_def.deployment.docker
|
||||
|
||||
# Check if container already exists
|
||||
@ -214,7 +214,7 @@ def start-docker-service [
|
||||
def start-docker-compose-service [
|
||||
service_def: record
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let compose_config = $service_def.deployment.docker_compose
|
||||
let compose_file = ($compose_config.compose_file | str replace -a '${HOME}' $env.HOME)
|
||||
|
||||
@ -249,7 +249,7 @@ def start-docker-compose-service [
|
||||
def start-kubernetes-service [
|
||||
service_def: record
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let k8s_config = $service_def.deployment.kubernetes
|
||||
|
||||
let kubeconfig = if "kubeconfig" in $k8s_config {
|
||||
@ -338,7 +338,7 @@ export def stop-service-by-mode [
|
||||
service_name: string
|
||||
service_def: record
|
||||
force: bool = false
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
match $service_def.deployment.mode {
|
||||
"binary" => {
|
||||
stop-binary-service $service_name $force
|
||||
@ -367,7 +367,7 @@ export def stop-service-by-mode [
|
||||
def stop-binary-service [
|
||||
service_name: string
|
||||
force: bool
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let pid_dir = (get-service-pid-dir)
|
||||
let pid_file = $"($pid_dir)/($service_name).pid"
|
||||
|
||||
@ -415,7 +415,7 @@ def stop-binary-service [
|
||||
def stop-docker-service [
|
||||
service_def: record
|
||||
force: bool
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let container_name = $service_def.deployment.docker.container_name
|
||||
|
||||
let result = (do {
|
||||
@ -438,7 +438,7 @@ def stop-docker-service [
|
||||
# Stop Docker Compose service
|
||||
def stop-docker-compose-service [
|
||||
service_def: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let compose_config = $service_def.deployment.docker_compose
|
||||
let compose_file = ($compose_config.compose_file | str replace -a '${HOME}' $env.HOME)
|
||||
let project_name = $compose_config.project_name? | default "provisioning"
|
||||
@ -460,7 +460,7 @@ def stop-docker-compose-service [
|
||||
def stop-kubernetes-service [
|
||||
service_def: record
|
||||
force: bool
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let k8s_config = $service_def.deployment.kubernetes
|
||||
|
||||
let kubeconfig = if "kubeconfig" in $k8s_config {
|
||||
@ -490,7 +490,7 @@ def stop-kubernetes-service [
|
||||
# Get service PID (for binary services)
|
||||
export def get-service-pid [
|
||||
service_name: string
|
||||
]: nothing -> int {
|
||||
] {
|
||||
let pid_dir = (get-service-pid-dir)
|
||||
let pid_file = $"($pid_dir)/[$service_name].pid"
|
||||
|
||||
@ -513,7 +513,7 @@ export def get-service-pid [
|
||||
export def kill-service-process [
|
||||
service_name: string
|
||||
signal: string = "TERM"
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let pid = (get-service-pid $service_name)
|
||||
|
||||
if $pid == 0 {
|
||||
|
||||
@ -5,20 +5,20 @@
|
||||
|
||||
use ../config/loader.nu *
|
||||
|
||||
def get-service-state-dir []: nothing -> string {
|
||||
def get-service-state-dir [] {
|
||||
$"($env.HOME)/.provisioning/services/state"
|
||||
}
|
||||
|
||||
def get-service-pid-dir []: nothing -> string {
|
||||
def get-service-pid-dir [] {
|
||||
$"($env.HOME)/.provisioning/services/pids"
|
||||
}
|
||||
|
||||
def get-service-log-dir []: nothing -> string {
|
||||
def get-service-log-dir [] {
|
||||
$"($env.HOME)/.provisioning/services/logs"
|
||||
}
|
||||
|
||||
# Load service registry from configuration
|
||||
export def load-service-registry []: nothing -> record {
|
||||
export def load-service-registry [] {
|
||||
let config = (load-provisioning-config)
|
||||
|
||||
# Load services from config file
|
||||
@ -40,7 +40,7 @@ export def load-service-registry []: nothing -> record {
|
||||
# Get service definition by name
|
||||
export def get-service-definition [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
if $service_name not-in ($registry | columns) {
|
||||
@ -60,7 +60,7 @@ export def get-service-definition [
|
||||
# Check if service is running
|
||||
export def is-service-running [
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
match $service_def.deployment.mode {
|
||||
@ -113,7 +113,7 @@ export def is-service-running [
|
||||
# Get service status
|
||||
export def get-service-status [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let is_running = (is-service-running $service_name)
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
@ -148,7 +148,7 @@ export def get-service-status [
|
||||
# Get service PID
|
||||
def get-service-pid [
|
||||
service_name: string
|
||||
]: nothing -> int {
|
||||
] {
|
||||
let pid_dir = (get-service-pid-dir)
|
||||
let pid_file = $"($pid_dir)/[$service_name].pid"
|
||||
|
||||
@ -170,7 +170,7 @@ def get-service-pid [
|
||||
# Get service uptime in seconds
|
||||
def get-service-uptime [
|
||||
service_name: string
|
||||
]: nothing -> int {
|
||||
] {
|
||||
let state_dir = (get-service-state-dir)
|
||||
let state_file = $"($state_dir)/[$service_name].json"
|
||||
|
||||
@ -201,7 +201,7 @@ def get-service-uptime [
|
||||
export def start-service [
|
||||
service_name: string
|
||||
--force (-f)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
# Ensure state directories exist
|
||||
mkdir (get-service-state-dir)
|
||||
mkdir (get-service-pid-dir)
|
||||
@ -261,7 +261,7 @@ export def start-service [
|
||||
export def stop-service [
|
||||
service_name: string
|
||||
--force (-f)
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
if not (is-service-running $service_name) {
|
||||
print $"Service '($service_name)' is not running"
|
||||
return true
|
||||
@ -302,7 +302,7 @@ export def stop-service [
|
||||
# Restart service
|
||||
export def restart-service [
|
||||
service_name: string
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
print $"Restarting service: ($service_name)"
|
||||
|
||||
if (is-service-running $service_name) {
|
||||
@ -316,7 +316,7 @@ export def restart-service [
|
||||
# Check service health
|
||||
export def check-service-health [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
use ./health.nu perform-health-check
|
||||
@ -327,13 +327,13 @@ export def check-service-health [
|
||||
export def wait-for-service-health [
|
||||
service_name: string
|
||||
timeout: int = 60
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
use ./health.nu wait-for-service
|
||||
wait-for-service $service_name $timeout
|
||||
}
|
||||
|
||||
# Get all services
|
||||
export def list-all-services []: nothing -> list {
|
||||
export def list-all-services [] {
|
||||
let registry = (load-service-registry)
|
||||
$registry | columns | each { |name|
|
||||
get-service-status $name
|
||||
@ -341,7 +341,7 @@ export def list-all-services []: nothing -> list {
|
||||
}
|
||||
|
||||
# Get running services
|
||||
export def list-running-services []: nothing -> list {
|
||||
export def list-running-services [] {
|
||||
list-all-services | where status == "running"
|
||||
}
|
||||
|
||||
@ -350,7 +350,7 @@ export def get-service-logs [
|
||||
service_name: string
|
||||
--lines: int = 50
|
||||
--follow (-f)
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let log_dir = (get-service-log-dir)
|
||||
let log_file = $"($log_dir)/($service_name).log"
|
||||
|
||||
@ -366,7 +366,7 @@ export def get-service-logs [
|
||||
}
|
||||
|
||||
# Initialize service state directories
|
||||
export def init-service-state []: nothing -> nothing {
|
||||
export def init-service-state [] {
|
||||
mkdir (get-service-state-dir)
|
||||
mkdir (get-service-pid-dir)
|
||||
mkdir (get-service-log-dir)
|
||||
|
||||
@ -9,7 +9,7 @@ use dependencies.nu [resolve-dependencies get-startup-order]
|
||||
# Check required services for operation
|
||||
export def check-required-services [
|
||||
operation: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
# Find all services required for this operation
|
||||
@ -34,7 +34,7 @@ export def check-required-services [
|
||||
}
|
||||
|
||||
# Check which services are running
|
||||
def partition-services [services: list, running: list, missing: list]: nothing -> record {
|
||||
def partition-services [services: list, running: list, missing: list] {
|
||||
if ($services | is-empty) {
|
||||
return { running: $running, missing: $missing }
|
||||
}
|
||||
@ -80,7 +80,7 @@ export def check-required-services [
|
||||
# Validate service prerequisites
|
||||
export def validate-service-prerequisites [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
# Check deployment mode requirements
|
||||
@ -121,7 +121,7 @@ export def validate-service-prerequisites [
|
||||
)
|
||||
|
||||
# Check dependencies
|
||||
def check-deps [deps: list, warnings: list]: nothing -> list {
|
||||
def check-deps [deps: list, warnings: list] {
|
||||
if ($deps | is-empty) {
|
||||
return $warnings
|
||||
}
|
||||
@ -138,7 +138,7 @@ export def validate-service-prerequisites [
|
||||
let warnings = (check-deps $service_def.dependencies [])
|
||||
|
||||
# Check conflicts
|
||||
def check-conflicts [conflicts: list, issues: list]: nothing -> list {
|
||||
def check-conflicts [conflicts: list, issues: list] {
|
||||
if ($conflicts | is-empty) {
|
||||
return $issues
|
||||
}
|
||||
@ -171,7 +171,7 @@ export def validate-service-prerequisites [
|
||||
# Auto-start required services
|
||||
export def auto-start-required-services [
|
||||
operation: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let check = (check-required-services $operation)
|
||||
|
||||
if $check.all_running {
|
||||
@ -196,7 +196,7 @@ export def auto-start-required-services [
|
||||
print $"Starting required services in order: ($startup_order | str join ' -> ')"
|
||||
|
||||
# Helper to start services in sequence
|
||||
def start-services-seq [services: list, started: list, failed: list]: nothing -> record {
|
||||
def start-services-seq [services: list, started: list, failed: list] {
|
||||
if ($services | is-empty) {
|
||||
return { started: $started, failed: $failed }
|
||||
}
|
||||
@ -238,11 +238,11 @@ export def auto-start-required-services [
|
||||
# Check service conflicts
|
||||
export def check-service-conflicts [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
# Helper to check conflicts
|
||||
def find-conflicts [conflicts: list, result: list]: nothing -> list {
|
||||
def find-conflicts [conflicts: list, result: list] {
|
||||
if ($conflicts | is-empty) {
|
||||
return $result
|
||||
}
|
||||
@ -276,7 +276,7 @@ export def check-service-conflicts [
|
||||
}
|
||||
|
||||
# Validate all services
|
||||
export def validate-all-services []: nothing -> record {
|
||||
export def validate-all-services [] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
let validation_results = (
|
||||
@ -304,7 +304,7 @@ export def validate-all-services []: nothing -> record {
|
||||
# Pre-flight check for service start
|
||||
export def preflight-start-service [
|
||||
service_name: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print $"Running pre-flight checks for ($service_name)..."
|
||||
|
||||
# 1. Validate prerequisites
|
||||
@ -331,7 +331,7 @@ export def preflight-start-service [
|
||||
let service_def = (get-service-definition $service_name)
|
||||
|
||||
# Helper to collect missing dependencies
|
||||
def collect-missing-deps [deps: list, missing: list]: nothing -> list {
|
||||
def collect-missing-deps [deps: list, missing: list] {
|
||||
if ($deps | is-empty) {
|
||||
return $missing
|
||||
}
|
||||
@ -375,7 +375,7 @@ export def preflight-start-service [
|
||||
}
|
||||
|
||||
# Get service readiness report
|
||||
export def get-readiness-report []: nothing -> record {
|
||||
export def get-readiness-report [] {
|
||||
let registry = (load-service-registry)
|
||||
|
||||
let services = (
|
||||
|
||||
@ -3,7 +3,7 @@ use ../config/accessor.nu *
|
||||
|
||||
export def env_file_providers [
|
||||
filepath: string
|
||||
]: nothing -> list {
|
||||
] {
|
||||
if not ($filepath | path exists) { return [] }
|
||||
(open $filepath | lines | find 'provisioning/providers/' |
|
||||
each {|it|
|
||||
@ -16,7 +16,7 @@ export def install_config [
|
||||
ops: string
|
||||
provisioning_cfg_name: string = "provisioning"
|
||||
--context
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
$env.PROVISIONING_DEBUG = ($env | get PROVISIONING_DEBUG? | default false | into bool)
|
||||
let reset = ($ops | str contains "reset")
|
||||
let use_context = if ($ops | str contains "context") or $context { true } else { false }
|
||||
|
||||
@ -9,7 +9,7 @@ use ./mod.nu *
|
||||
# ============================================================================
|
||||
|
||||
# Check if Docker is installed and running
|
||||
export def has-docker []: nothing -> bool {
|
||||
export def has-docker [] {
|
||||
let which_check = (bash -c "which docker > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
if ($which_check != 0) {
|
||||
return false
|
||||
@ -20,55 +20,55 @@ export def has-docker []: nothing -> bool {
|
||||
}
|
||||
|
||||
# Check if Kubernetes (kubectl) is installed
|
||||
export def has-kubectl []: nothing -> bool {
|
||||
export def has-kubectl [] {
|
||||
let kubectl_check = (bash -c "which kubectl > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($kubectl_check == 0)
|
||||
}
|
||||
|
||||
# Check if Docker Compose is installed
|
||||
export def has-docker-compose []: nothing -> bool {
|
||||
export def has-docker-compose [] {
|
||||
let compose_check = (bash -c "docker compose version > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($compose_check == 0)
|
||||
}
|
||||
|
||||
# Check if Podman is installed
|
||||
export def has-podman []: nothing -> bool {
|
||||
export def has-podman [] {
|
||||
let podman_check = (bash -c "which podman > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($podman_check == 0)
|
||||
}
|
||||
|
||||
# Check if systemd is available
|
||||
export def has-systemd []: nothing -> bool {
|
||||
export def has-systemd [] {
|
||||
let systemctl_check = (bash -c "systemctl --version > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($systemctl_check == 0)
|
||||
}
|
||||
|
||||
# Check if SSH is available
|
||||
export def has-ssh []: nothing -> bool {
|
||||
export def has-ssh [] {
|
||||
let ssh_check = (bash -c "which ssh > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($ssh_check == 0)
|
||||
}
|
||||
|
||||
# Check if Nickel is installed
|
||||
export def has-nickel []: nothing -> bool {
|
||||
let decl_check = (bash -c "which nickel > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
export def has-nickel [] {
|
||||
let nickel_check = (bash -c "which nickel > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($nickel_check == 0)
|
||||
}
|
||||
|
||||
# Check if SOPS is installed
|
||||
export def has-sops []: nothing -> bool {
|
||||
export def has-sops [] {
|
||||
let sops_check = (bash -c "which sops > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($sops_check == 0)
|
||||
}
|
||||
|
||||
# Check if Age is installed
|
||||
export def has-age []: nothing -> bool {
|
||||
export def has-age [] {
|
||||
let age_check = (bash -c "which age > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($age_check == 0)
|
||||
}
|
||||
|
||||
# Get detailed deployment capabilities
|
||||
export def get-deployment-capabilities []: nothing -> record {
|
||||
export def get-deployment-capabilities [] {
|
||||
{
|
||||
docker_available: (has-docker)
|
||||
docker_compose_available: (has-docker-compose)
|
||||
@ -89,7 +89,7 @@ export def get-deployment-capabilities []: nothing -> record {
|
||||
# Check if port is available
|
||||
export def is-port-available [
|
||||
port: int
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let os_type = (detect-os)
|
||||
|
||||
let port_check = if $os_type == "macos" {
|
||||
@ -105,7 +105,7 @@ export def is-port-available [
|
||||
export def get-available-ports [
|
||||
start_port: int
|
||||
end_port: int
|
||||
]: nothing -> list<int> {
|
||||
] {
|
||||
mut available = []
|
||||
|
||||
for port in ($start_port..$end_port) {
|
||||
@ -118,7 +118,7 @@ export def get-available-ports [
|
||||
}
|
||||
|
||||
# Check internet connectivity
|
||||
export def has-internet-connectivity []: nothing -> bool {
|
||||
export def has-internet-connectivity [] {
|
||||
let curl_check = (bash -c "curl -s -I --max-time 3 https://www.google.com > /dev/null 2>&1; echo $?" | str trim | into int)
|
||||
($curl_check == 0)
|
||||
}
|
||||
@ -128,7 +128,7 @@ export def has-internet-connectivity []: nothing -> bool {
|
||||
# ============================================================================
|
||||
|
||||
# Check if provisioning is already configured
|
||||
export def is-provisioning-configured []: nothing -> bool {
|
||||
export def is-provisioning-configured [] {
|
||||
let config_base = (get-config-base-path)
|
||||
let system_config = $"($config_base)/system.toml"
|
||||
|
||||
@ -136,7 +136,7 @@ export def is-provisioning-configured []: nothing -> bool {
|
||||
}
|
||||
|
||||
# Get existing provisioning configuration summary
|
||||
export def get-existing-config-summary []: nothing -> record {
|
||||
export def get-existing-config-summary [] {
|
||||
let config_base = (get-config-base-path)
|
||||
let system_config_exists = ($"($config_base)/system.toml" | path exists)
|
||||
let workspaces_exists = ($"($config_base)/workspaces" | path exists)
|
||||
@ -155,28 +155,28 @@ export def get-existing-config-summary []: nothing -> record {
|
||||
# ============================================================================
|
||||
|
||||
# Check if orchestrator is running
|
||||
export def is-orchestrator-running []: nothing -> bool {
|
||||
export def is-orchestrator-running [] {
|
||||
let endpoint = "http://localhost:9090/health"
|
||||
let result = (do { curl -s -f --max-time 2 $endpoint o> /dev/null e> /dev/null } | complete)
|
||||
($result.exit_code == 0)
|
||||
}
|
||||
|
||||
# Check if control-center is running
|
||||
export def is-control-center-running []: nothing -> bool {
|
||||
export def is-control-center-running [] {
|
||||
let endpoint = "http://localhost:3000/health"
|
||||
let result = (do { curl -s -f --max-time 2 $endpoint o> /dev/null e> /dev/null } | complete)
|
||||
($result.exit_code == 0)
|
||||
}
|
||||
|
||||
# Check if KMS service is running
|
||||
export def is-kms-running []: nothing -> bool {
|
||||
export def is-kms-running [] {
|
||||
let endpoint = "http://localhost:3001/health"
|
||||
let result = (do { curl -s -f --max-time 2 $endpoint o> /dev/null e> /dev/null } | complete)
|
||||
($result.exit_code == 0)
|
||||
}
|
||||
|
||||
# Get platform services status
|
||||
export def get-platform-services-status []: nothing -> record {
|
||||
export def get-platform-services-status [] {
|
||||
{
|
||||
orchestrator_running: (is-orchestrator-running)
|
||||
orchestrator_endpoint: "http://localhost:9090/health"
|
||||
@ -192,7 +192,7 @@ export def get-platform-services-status []: nothing -> record {
|
||||
# ============================================================================
|
||||
|
||||
# Generate comprehensive environment detection report
|
||||
export def generate-detection-report []: nothing -> record {
|
||||
export def generate-detection-report [] {
|
||||
{
|
||||
system: {
|
||||
os: (detect-os)
|
||||
@ -220,7 +220,7 @@ export def generate-detection-report []: nothing -> record {
|
||||
# Print detection report in readable format
|
||||
export def print-detection-report [
|
||||
report: record
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print ""
|
||||
print "╔═══════════════════════════════════════════════════════════════╗"
|
||||
print "║ ENVIRONMENT DETECTION REPORT ║"
|
||||
@ -281,7 +281,7 @@ export def print-detection-report [
|
||||
# Recommend deployment mode based on available capabilities
|
||||
export def recommend-deployment-mode [
|
||||
report: record
|
||||
]: nothing -> string {
|
||||
] {
|
||||
let caps = $report.capabilities
|
||||
|
||||
if ($caps.docker_available and $caps.docker_compose_available) {
|
||||
@ -300,7 +300,7 @@ export def recommend-deployment-mode [
|
||||
# Get recommended deployment configuration
|
||||
export def get-recommended-config [
|
||||
report: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let deployment_mode = (recommend-deployment-mode $report)
|
||||
let caps = $report.capabilities
|
||||
|
||||
@ -324,7 +324,7 @@ export def get-recommended-config [
|
||||
# Get list of missing required tools
|
||||
export def get-missing-required-tools [
|
||||
report: record
|
||||
]: nothing -> list<string> {
|
||||
] {
|
||||
mut missing = []
|
||||
|
||||
if not $report.capabilities.nickel_available {
|
||||
|
||||
@ -1,408 +0,0 @@
|
||||
# Configuration Migration Module
|
||||
# Handles migration from existing workspace configurations to new setup system
|
||||
# Follows Nushell guidelines: explicit types, single purpose, no try-catch
|
||||
|
||||
use ./mod.nu *
|
||||
use ./detection.nu *
|
||||
|
||||
# ============================================================================
|
||||
# EXISTING CONFIGURATION DETECTION
|
||||
# ============================================================================
|
||||
|
||||
# Detect existing workspace configuration
|
||||
export def detect-existing-workspace [
|
||||
workspace_path: string
|
||||
]: nothing -> record {
|
||||
let config_path = $"($workspace_path)/config/provisioning.yaml"
|
||||
let providers_path = $"($workspace_path)/.providers"
|
||||
let infra_path = $"($workspace_path)/infra"
|
||||
|
||||
{
|
||||
workspace_path: $workspace_path
|
||||
has_config: ($config_path | path exists)
|
||||
config_path: $config_path
|
||||
has_providers: ($providers_path | path exists)
|
||||
providers_path: $providers_path
|
||||
has_infra: ($infra_path | path exists)
|
||||
infra_path: $infra_path
|
||||
}
|
||||
}
|
||||
|
||||
# Find existing workspace directories
|
||||
export def find-existing-workspaces []: nothing -> list<string> {
|
||||
mut workspaces = []
|
||||
|
||||
# Check common workspace locations
|
||||
let possible_paths = [
|
||||
"workspace_librecloud"
|
||||
"./workspace_librecloud"
|
||||
"../workspace_librecloud"
|
||||
"workspaces"
|
||||
"./workspaces"
|
||||
]
|
||||
|
||||
for path in $possible_paths {
|
||||
let expanded_path = ($path | path expand)
|
||||
if ($expanded_path | path exists) and (($expanded_path | path type) == "dir") {
|
||||
let workspace_config = $"($expanded_path)/config/provisioning.yaml"
|
||||
if ($workspace_config | path exists) {
|
||||
$workspaces = ($workspaces | append $expanded_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$workspaces
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION MIGRATION
|
||||
# ============================================================================
|
||||
|
||||
# Migrate workspace configuration from YAML to new system
|
||||
export def migrate-workspace-config [
|
||||
workspace_path: string
|
||||
config_base: string
|
||||
--backup = true
|
||||
]: nothing -> record {
|
||||
let source_config = $"($workspace_path)/config/provisioning.yaml"
|
||||
|
||||
if not ($source_config | path exists) {
|
||||
return {
|
||||
success: false
|
||||
error: "Source configuration not found"
|
||||
}
|
||||
}
|
||||
|
||||
# Load existing configuration
|
||||
let existing_config = (load-config-yaml $source_config)
|
||||
|
||||
# Extract workspace name from path
|
||||
let workspace_name = ($workspace_path | path basename)
|
||||
|
||||
# Create backup if requested
|
||||
if $backup {
|
||||
let timestamp_for_backup = (get-timestamp-iso8601 | str replace -a ':' '-')
|
||||
let backup_path = $"($config_base)/migration-backup-($workspace_name)-($timestamp_for_backup).yaml"
|
||||
let backup_result = (do { cp $source_config $backup_path } | complete)
|
||||
|
||||
if ($backup_result.exit_code != 0) {
|
||||
print-setup-warning $"Failed to create backup at ($backup_path)"
|
||||
} else {
|
||||
print-setup-success $"Configuration backed up to ($backup_path)"
|
||||
}
|
||||
}
|
||||
|
||||
# Create migration record
|
||||
{
|
||||
success: true
|
||||
workspace_name: $workspace_name
|
||||
source_path: $source_config
|
||||
migrated_at: (get-timestamp-iso8601)
|
||||
backup_created: $backup
|
||||
}
|
||||
}
|
||||
|
||||
# Migrate provider configurations
|
||||
export def migrate-provider-configs [
|
||||
workspace_path: string
|
||||
config_base: string
|
||||
]: nothing -> record {
|
||||
let providers_source = $"($workspace_path)/.providers"
|
||||
|
||||
if not ($providers_source | path exists) {
|
||||
return {
|
||||
success: false
|
||||
migrated_providers: []
|
||||
error: "No provider directory found"
|
||||
}
|
||||
}
|
||||
|
||||
mut migrated = []
|
||||
|
||||
# Get list of provider directories
|
||||
let result = (do {
|
||||
ls $providers_source | where type == "dir"
|
||||
} | complete)
|
||||
|
||||
if ($result.exit_code != 0) {
|
||||
return {
|
||||
success: false
|
||||
migrated_providers: []
|
||||
error: "Failed to read provider directories"
|
||||
}
|
||||
}
|
||||
|
||||
# Migrate each provider
|
||||
for provider_entry in $result.stdout {
|
||||
let provider_name = ($provider_entry | str trim)
|
||||
if ($provider_name | str length) > 0 {
|
||||
print-setup-info $"Migrating provider: ($provider_name)"
|
||||
$migrated = ($migrated | append $provider_name)
|
||||
}
|
||||
}
|
||||
|
||||
let success_status = ($migrated | length) > 0
|
||||
let migrated_at_value = (get-timestamp-iso8601)
|
||||
{
|
||||
success: $success_status
|
||||
migrated_providers: $migrated
|
||||
source_path: $providers_source
|
||||
migrated_at: $migrated_at_value
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MIGRATION VALIDATION
|
||||
# ============================================================================
|
||||
|
||||
# Validate migration can proceed safely
|
||||
export def validate-migration [
|
||||
workspace_path: string
|
||||
config_base: string
|
||||
]: nothing -> record {
|
||||
mut warnings = []
|
||||
mut errors = []
|
||||
|
||||
# Check source workspace exists
|
||||
if not ($workspace_path | path exists) {
|
||||
$errors = ($errors | append "Source workspace path does not exist")
|
||||
}
|
||||
|
||||
# Check configuration base exists
|
||||
if not ($config_base | path exists) {
|
||||
$errors = ($errors | append "Target configuration base does not exist")
|
||||
}
|
||||
|
||||
# Check if migration already happened
|
||||
let migration_marker = $"($config_base)/migration_completed.yaml"
|
||||
if ($migration_marker | path exists) {
|
||||
$warnings = ($warnings | append "Migration appears to have been run before")
|
||||
}
|
||||
|
||||
# Check for conflicts
|
||||
let workspace_name = ($workspace_path | path basename)
|
||||
let registry_path = $"($config_base)/workspaces_registry.yaml"
|
||||
|
||||
if ($registry_path | path exists) {
|
||||
let registry = (load-config-yaml $registry_path)
|
||||
if ($registry.workspaces? | default [] | any { |w| $w.name == $workspace_name }) {
|
||||
$warnings = ($warnings | append $"Workspace '($workspace_name)' already registered")
|
||||
}
|
||||
}
|
||||
|
||||
let can_proceed_status = ($errors | length) == 0
|
||||
let error_count_value = ($errors | length)
|
||||
let warning_count_value = ($warnings | length)
|
||||
{
|
||||
can_proceed: $can_proceed_status
|
||||
errors: $errors
|
||||
warnings: $warnings
|
||||
error_count: $error_count_value
|
||||
warning_count: $warning_count_value
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MIGRATION EXECUTION
|
||||
# ============================================================================
|
||||
|
||||
# Execute complete workspace migration
|
||||
export def execute-migration [
|
||||
workspace_path: string
|
||||
config_base: string = ""
|
||||
--backup = true
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
print-setup-header "Workspace Configuration Migration"
|
||||
print ""
|
||||
|
||||
# Validate migration can proceed
|
||||
let validation = (validate-migration $workspace_path $base)
|
||||
if not $validation.can_proceed {
|
||||
for error in $validation.errors {
|
||||
print-setup-error $error
|
||||
}
|
||||
return {
|
||||
success: false
|
||||
errors: $validation.errors
|
||||
}
|
||||
}
|
||||
|
||||
# Show warnings
|
||||
if ($validation.warnings | length) > 0 {
|
||||
for warning in $validation.warnings {
|
||||
print-setup-warning $warning
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
print-setup-info "Starting migration process..."
|
||||
print ""
|
||||
|
||||
# Step 1: Migrate workspace configuration
|
||||
print-setup-info "Migrating workspace configuration..."
|
||||
let config_migration = (migrate-workspace-config $workspace_path $base --backup=$backup)
|
||||
if not $config_migration.success {
|
||||
print-setup-error $config_migration.error
|
||||
return {
|
||||
success: false
|
||||
error: $config_migration.error
|
||||
}
|
||||
}
|
||||
print-setup-success "Workspace configuration migrated"
|
||||
|
||||
# Step 2: Migrate provider configurations
|
||||
print-setup-info "Migrating provider configurations..."
|
||||
let provider_migration = (migrate-provider-configs $workspace_path $base)
|
||||
if $provider_migration.success {
|
||||
print-setup-success $"Migrated ($provider_migration.migrated_providers | length) providers"
|
||||
} else {
|
||||
print-setup-warning "No provider configurations to migrate"
|
||||
}
|
||||
|
||||
# Step 3: Create migration marker
|
||||
let workspace_name = ($workspace_path | path basename)
|
||||
let migration_marker_path = $"($base)/migration_completed.yaml"
|
||||
let migration_record = {
|
||||
version: "1.0.0"
|
||||
completed_at: (get-timestamp-iso8601)
|
||||
workspace_migrated: $workspace_name
|
||||
source_path: $workspace_path
|
||||
target_path: $base
|
||||
backup_created: $backup
|
||||
}
|
||||
|
||||
let save_result = (save-config-yaml $migration_marker_path $migration_record)
|
||||
if not $save_result {
|
||||
print-setup-warning "Failed to create migration marker"
|
||||
}
|
||||
|
||||
print ""
|
||||
print-setup-success "Migration completed successfully!"
|
||||
print ""
|
||||
|
||||
# Summary
|
||||
print "Migration Summary:"
|
||||
print $" Source Workspace: ($workspace_path)"
|
||||
print $" Target Config Base: ($base)"
|
||||
print $" Configuration Migrated: ✅"
|
||||
print $" Providers Migrated: ($provider_migration.migrated_providers | length)"
|
||||
if $backup {
|
||||
print " Backup Created: ✅"
|
||||
}
|
||||
print ""
|
||||
|
||||
{
|
||||
success: true
|
||||
workspace_name: $workspace_name
|
||||
config_migration: $config_migration
|
||||
provider_migration: $provider_migration
|
||||
migration_completed_at: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# MIGRATION ROLLBACK
|
||||
# ============================================================================
|
||||
|
||||
# Rollback migration from backup
|
||||
export def rollback-migration [
|
||||
workspace_name: string
|
||||
config_base: string = ""
|
||||
--restore_backup = true
|
||||
]: nothing -> record {
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
print-setup-header "Rolling Back Migration"
|
||||
print ""
|
||||
print-setup-warning "Initiating migration rollback..."
|
||||
print ""
|
||||
|
||||
# Find and restore backup
|
||||
let migration_marker = $"($base)/migration_completed.yaml"
|
||||
if not ($migration_marker | path exists) {
|
||||
print-setup-error "No migration record found - cannot rollback"
|
||||
return {
|
||||
success: false
|
||||
error: "No migration record found"
|
||||
}
|
||||
}
|
||||
|
||||
let migration_record = (load-config-yaml $migration_marker)
|
||||
|
||||
# Find backup file
|
||||
let backup_pattern = $"($base)/migration-backup-($workspace_name)-*.yaml"
|
||||
print-setup-info $"Looking for backup matching: ($backup_pattern)"
|
||||
|
||||
# Remove migration artifacts
|
||||
if ($migration_marker | path exists) {
|
||||
let rm_result = (do { rm $migration_marker } | complete)
|
||||
if ($rm_result.exit_code == 0) {
|
||||
print-setup-success "Migration marker removed"
|
||||
}
|
||||
}
|
||||
|
||||
print ""
|
||||
print-setup-success "Migration rollback completed"
|
||||
print ""
|
||||
print "Note: Please verify your workspace is in the desired state"
|
||||
|
||||
{
|
||||
success: true
|
||||
workspace_name: $workspace_name
|
||||
rolled_back_at: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# AUTO-MIGRATION
|
||||
# ============================================================================
|
||||
|
||||
# Automatically detect and migrate existing workspaces
|
||||
export def auto-migrate-existing [
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
print-setup-header "Detecting Existing Workspaces"
|
||||
print ""
|
||||
|
||||
# Find existing workspaces
|
||||
let existing = (find-existing-workspaces)
|
||||
|
||||
if ($existing | length) == 0 {
|
||||
print-setup-info "No existing workspaces detected"
|
||||
return {
|
||||
success: true
|
||||
workspaces_found: 0
|
||||
workspaces: []
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-success $"Found ($existing | length) existing workspace(s)"
|
||||
print ""
|
||||
|
||||
mut migrated = []
|
||||
|
||||
for workspace_path in $existing {
|
||||
let workspace_name = ($workspace_path | path basename)
|
||||
print-setup-info $"Auto-migrating: ($workspace_name)"
|
||||
|
||||
let migration_result = (execute-migration $workspace_path $base --verbose=$verbose)
|
||||
if $migration_result.success {
|
||||
$migrated = ($migrated | append $workspace_name)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
success: true
|
||||
workspaces_found: ($existing | length)
|
||||
workspaces: $existing
|
||||
migrated_count: ($migrated | length)
|
||||
migrated_workspaces: $migrated
|
||||
timestamp: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ export use config.nu *
|
||||
# ============================================================================
|
||||
|
||||
# Get OS-appropriate base configuration directory
|
||||
export def get-config-base-path []: nothing -> string {
|
||||
export def get-config-base-path [] {
|
||||
match $nu.os-info.name {
|
||||
"macos" => {
|
||||
let home = ($env.HOME? | default "~" | path expand)
|
||||
@ -33,18 +33,18 @@ export def get-config-base-path []: nothing -> string {
|
||||
}
|
||||
|
||||
# Get provisioning installation path
|
||||
export def get-install-path []: nothing -> string {
|
||||
export def get-install-path [] {
|
||||
config-get "setup.install_path" (get-base-path)
|
||||
}
|
||||
|
||||
# Get global workspaces directory
|
||||
export def get-workspaces-dir []: nothing -> string {
|
||||
export def get-workspaces-dir [] {
|
||||
let config_base = (get-config-base-path)
|
||||
$"($config_base)/workspaces"
|
||||
}
|
||||
|
||||
# Get cache directory
|
||||
export def get-cache-dir []: nothing -> string {
|
||||
export def get-cache-dir [] {
|
||||
let config_base = (get-config-base-path)
|
||||
$"($config_base)/cache"
|
||||
}
|
||||
@ -54,7 +54,7 @@ export def get-cache-dir []: nothing -> string {
|
||||
# ============================================================================
|
||||
|
||||
# Ensure configuration directories exist
|
||||
export def ensure-config-dirs []: nothing -> bool {
|
||||
export def ensure-config-dirs [] {
|
||||
let config_base = (get-config-base-path)
|
||||
let workspaces_dir = (get-workspaces-dir)
|
||||
let cache_dir = (get-cache-dir)
|
||||
@ -81,7 +81,7 @@ export def ensure-config-dirs []: nothing -> bool {
|
||||
# Load TOML configuration file
|
||||
export def load-config-toml [
|
||||
file_path: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
if ($file_path | path exists) {
|
||||
let file_content = (open $file_path)
|
||||
match ($file_content | type) {
|
||||
@ -100,7 +100,7 @@ export def load-config-toml [
|
||||
export def save-config-toml [
|
||||
file_path: string
|
||||
config: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do { $config | to toml | save -f $file_path } | complete)
|
||||
($result.exit_code == 0)
|
||||
}
|
||||
@ -108,7 +108,7 @@ export def save-config-toml [
|
||||
# Load YAML configuration file
|
||||
export def load-config-yaml [
|
||||
file_path: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
if ($file_path | path exists) {
|
||||
let file_content = (open $file_path)
|
||||
match ($file_content | type) {
|
||||
@ -127,7 +127,7 @@ export def load-config-yaml [
|
||||
export def save-config-yaml [
|
||||
file_path: string
|
||||
config: record
|
||||
]: nothing -> bool {
|
||||
] {
|
||||
let result = (do { $config | to yaml | save -f $file_path } | complete)
|
||||
($result.exit_code == 0)
|
||||
}
|
||||
@ -137,17 +137,17 @@ export def save-config-yaml [
|
||||
# ============================================================================
|
||||
|
||||
# Detect operating system
|
||||
export def detect-os []: nothing -> string {
|
||||
export def detect-os [] {
|
||||
$nu.os-info.name
|
||||
}
|
||||
|
||||
# Get system architecture
|
||||
export def detect-architecture []: nothing -> string {
|
||||
export def detect-architecture [] {
|
||||
$env.PROCESSOR_ARCHITECTURE? | default $nu.os-info.arch
|
||||
}
|
||||
|
||||
# Get CPU count
|
||||
export def get-cpu-count []: nothing -> int {
|
||||
export def get-cpu-count [] {
|
||||
let result = (do {
|
||||
match (detect-os) {
|
||||
"macos" => { ^sysctl -n hw.ncpu }
|
||||
@ -168,7 +168,7 @@ export def get-cpu-count []: nothing -> int {
|
||||
}
|
||||
|
||||
# Get system memory in GB
|
||||
export def get-system-memory-gb []: nothing -> int {
|
||||
export def get-system-memory-gb [] {
|
||||
let result = (do {
|
||||
match (detect-os) {
|
||||
"macos" => { ^sysctl -n hw.memsize }
|
||||
@ -197,7 +197,7 @@ export def get-system-memory-gb []: nothing -> int {
|
||||
}
|
||||
|
||||
# Get system disk space in GB
|
||||
export def get-system-disk-gb []: nothing -> int {
|
||||
export def get-system-disk-gb [] {
|
||||
let home_dir = ($env.HOME? | default "~" | path expand)
|
||||
let result = (do {
|
||||
^df -H $home_dir | tail -n 1 | awk '{print $2}'
|
||||
@ -212,17 +212,17 @@ export def get-system-disk-gb []: nothing -> int {
|
||||
}
|
||||
|
||||
# Get current timestamp in ISO 8601 format
|
||||
export def get-timestamp-iso8601 []: nothing -> string {
|
||||
export def get-timestamp-iso8601 [] {
|
||||
(date now | format date "%Y-%m-%dT%H:%M:%SZ")
|
||||
}
|
||||
|
||||
# Get current user
|
||||
export def get-current-user []: nothing -> string {
|
||||
export def get-current-user [] {
|
||||
$env.USER? | default $env.USERNAME? | default "unknown"
|
||||
}
|
||||
|
||||
# Get system hostname
|
||||
export def get-system-hostname []: nothing -> string {
|
||||
export def get-system-hostname [] {
|
||||
let result = (do { ^hostname } | complete)
|
||||
|
||||
if ($result.exit_code == 0) {
|
||||
@ -239,7 +239,7 @@ export def get-system-hostname []: nothing -> string {
|
||||
# Print setup section header
|
||||
export def print-setup-header [
|
||||
title: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print ""
|
||||
print $"🔧 ($title)"
|
||||
print "════════════════════════════════════════════════════════════════"
|
||||
@ -248,28 +248,28 @@ export def print-setup-header [
|
||||
# Print setup success message
|
||||
export def print-setup-success [
|
||||
message: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"✅ ($message)"
|
||||
}
|
||||
|
||||
# Print setup warning message
|
||||
export def print-setup-warning [
|
||||
message: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"⚠️ ($message)"
|
||||
}
|
||||
|
||||
# Print setup error message
|
||||
export def print-setup-error [
|
||||
message: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"❌ ($message)"
|
||||
}
|
||||
|
||||
# Print setup info message
|
||||
export def print-setup-info [
|
||||
message: string
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
print $"ℹ️ ($message)"
|
||||
}
|
||||
|
||||
@ -282,7 +282,7 @@ export def setup-dispatch [
|
||||
command: string
|
||||
args: list<string>
|
||||
--verbose = false
|
||||
]: nothing -> nothing {
|
||||
] {
|
||||
|
||||
# Ensure config directories exist before any setup operation
|
||||
if not (ensure-config-dirs) {
|
||||
@ -348,11 +348,11 @@ export def setup-dispatch [
|
||||
# ============================================================================
|
||||
|
||||
# Initialize setup module
|
||||
export def setup-init []: nothing -> bool {
|
||||
export def setup-init [] {
|
||||
ensure-config-dirs
|
||||
}
|
||||
|
||||
# Get setup module version
|
||||
export def get-setup-version []: nothing -> string {
|
||||
export def get-setup-version [] {
|
||||
"1.0.0"
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ use ../platform/bootstrap.nu *
|
||||
# Validate deployment mode is supported
|
||||
export def validate-deployment-mode [
|
||||
mode: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let valid_modes = ["docker-compose", "kubernetes", "remote-ssh", "systemd"]
|
||||
let is_valid = ($mode | inside $valid_modes)
|
||||
|
||||
@ -29,7 +29,7 @@ export def validate-deployment-mode [
|
||||
# Check deployment mode support on current system
|
||||
export def check-deployment-mode-support [
|
||||
mode: string
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let support = (match $mode {
|
||||
"docker-compose" => {
|
||||
let docker_ok = (has-docker)
|
||||
@ -88,7 +88,7 @@ export def reserve-service-ports [
|
||||
orchestrator_port: int = 9090
|
||||
control_center_port: int = 3000
|
||||
kms_port: int = 3001
|
||||
]: nothing -> record {
|
||||
] {
|
||||
mut reserved_ports = []
|
||||
mut port_conflicts = []
|
||||
|
||||
@ -132,7 +132,7 @@ export def start-platform-services [
|
||||
deployment_mode: string
|
||||
--auto_start = true
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
# Validate deployment mode
|
||||
let mode_validation = (validate-deployment-mode $deployment_mode)
|
||||
if not $mode_validation.valid {
|
||||
@ -186,7 +186,7 @@ export def start-platform-services [
|
||||
export def apply-platform-config [
|
||||
config_base: string
|
||||
config_data: record
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let deployment_config_path = $"($config_base)/platform/deployment.toml"
|
||||
|
||||
# Load current deployment config if it exists
|
||||
@ -222,7 +222,7 @@ export def apply-platform-config [
|
||||
# ============================================================================
|
||||
|
||||
# Verify platform services are running
|
||||
export def verify-platform-services []: nothing -> record {
|
||||
export def verify-platform-services [] {
|
||||
let orch_health = (do { curl -s -f http://localhost:9090/health o> /dev/null e> /dev/null } | complete).exit_code == 0
|
||||
let cc_health = (do { curl -s -f http://localhost:3000/health o> /dev/null e> /dev/null } | complete).exit_code == 0
|
||||
let kms_health = (do { curl -s -f http://localhost:3001/health o> /dev/null e> /dev/null } | complete).exit_code == 0
|
||||
@ -252,7 +252,7 @@ export def verify-platform-services []: nothing -> record {
|
||||
export def setup-platform-solo [
|
||||
config_base: string
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print-setup-header "Setting up Platform (Solo Mode)"
|
||||
print ""
|
||||
print "Solo mode: Single-user local development setup"
|
||||
@ -296,7 +296,7 @@ export def setup-platform-solo [
|
||||
export def setup-platform-multiuser [
|
||||
config_base: string
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print-setup-header "Setting up Platform (Multi-user Mode)"
|
||||
print ""
|
||||
print "Multi-user mode: Shared team environment"
|
||||
@ -352,7 +352,7 @@ export def setup-platform-multiuser [
|
||||
export def setup-platform-cicd [
|
||||
config_base: string
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
print-setup-header "Setting up Platform (CI/CD Mode)"
|
||||
print ""
|
||||
print "CI/CD mode: Automated deployment pipeline setup"
|
||||
@ -396,34 +396,261 @@ export def setup-platform-cicd [
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# PROFILE-BASED SETUP (NICKEL-ALWAYS)
|
||||
# ============================================================================
|
||||
|
||||
# Setup platform for developer profile (fast, local, type-safe)
|
||||
export def setup-platform-developer [
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
] {
|
||||
print-setup-header "Setting up Platform (Developer Profile)"
|
||||
print ""
|
||||
print "Developer profile: Fast local setup with type-safe Nickel validation"
|
||||
print ""
|
||||
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
# Check Docker availability
|
||||
if not (has-docker) {
|
||||
print-setup-error "Docker is required for developer profile"
|
||||
return {
|
||||
success: false
|
||||
error: "Docker not installed"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info "Generating Nickel platform configuration..."
|
||||
if not (create-platform-config-nickel $base "docker-compose" "developer") {
|
||||
print-setup-error "Failed to generate Nickel platform config"
|
||||
return {
|
||||
success: false
|
||||
error: "Failed to generate Nickel platform config"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info "Validating Nickel configuration..."
|
||||
let validation = (validate-nickel-config $"($base)/platform/deployment.ncl")
|
||||
if not $validation {
|
||||
print-setup-error "Nickel validation failed"
|
||||
return {
|
||||
success: false
|
||||
error: "Nickel validation failed"
|
||||
}
|
||||
}
|
||||
|
||||
# Reserve ports
|
||||
let port_check = (reserve-service-ports)
|
||||
if not $port_check.all_available {
|
||||
print-setup-warning $"Port conflicts: ($port_check.conflicts | str join ', ')"
|
||||
}
|
||||
|
||||
# Start services
|
||||
let start_result = (start-platform-services "docker-compose" --verbose=$verbose)
|
||||
|
||||
{
|
||||
success: $start_result.success
|
||||
profile: "developer"
|
||||
deployment: "docker-compose"
|
||||
config_base: $base
|
||||
timestamp: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
# Setup platform for production profile (validated, secure, HA)
|
||||
export def setup-platform-production [
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
] {
|
||||
print-setup-header "Setting up Platform (Production Profile)"
|
||||
print ""
|
||||
print "Production profile: Validated deployment with security and HA"
|
||||
print ""
|
||||
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
# Check Kubernetes availability (preferred for production)
|
||||
let deployment_mode = if (has-kubectl) {
|
||||
"kubernetes"
|
||||
} else if (has-docker-compose) {
|
||||
"docker-compose"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if ($deployment_mode == "") {
|
||||
print-setup-error "Kubernetes or Docker Compose required for production profile"
|
||||
return {
|
||||
success: false
|
||||
error: "Missing required tools"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info $"Using deployment mode: ($deployment_mode)"
|
||||
|
||||
# Check Nickel is available for production-grade validation
|
||||
let nickel_check = (do { which nickel } | complete)
|
||||
if ($nickel_check.exit_code != 0) {
|
||||
print-setup-warning "Nickel not installed - validation will be skipped (recommended to install for production)"
|
||||
}
|
||||
|
||||
print-setup-info "Generating Nickel platform configuration..."
|
||||
if not (create-platform-config-nickel $base $deployment_mode "production") {
|
||||
print-setup-error "Failed to generate Nickel platform config"
|
||||
return {
|
||||
success: false
|
||||
error: "Failed to generate Nickel platform config"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info "Validating Nickel configuration..."
|
||||
let validation = (validate-nickel-config $"($base)/platform/deployment.ncl")
|
||||
if not $validation {
|
||||
print-setup-error "Nickel validation failed"
|
||||
return {
|
||||
success: false
|
||||
error: "Nickel validation failed"
|
||||
}
|
||||
}
|
||||
|
||||
# Pre-flight checks for production
|
||||
print-setup-info "Running production pre-flight checks..."
|
||||
let cpu_count = (get-cpu-count)
|
||||
let memory_gb = (get-system-memory-gb)
|
||||
|
||||
if ($deployment_mode == "kubernetes") {
|
||||
if ($cpu_count < 4) {
|
||||
print-setup-warning "Production Kubernetes deployment recommended with at least 4 CPUs"
|
||||
}
|
||||
if ($memory_gb < 8) {
|
||||
print-setup-warning "Production Kubernetes deployment recommended with at least 8GB RAM"
|
||||
}
|
||||
}
|
||||
|
||||
# Reserve ports
|
||||
let port_check = (reserve-service-ports)
|
||||
if not $port_check.all_available {
|
||||
print-setup-warning $"Port conflicts: ($port_check.conflicts | str join ', ')"
|
||||
}
|
||||
|
||||
# Start services
|
||||
let start_result = (start-platform-services $deployment_mode --verbose=$verbose)
|
||||
|
||||
{
|
||||
success: $start_result.success
|
||||
profile: "production"
|
||||
deployment: $deployment_mode
|
||||
config_base: $base
|
||||
timestamp: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
# Setup platform for CI/CD profile (ephemeral, automated, fast)
|
||||
export def setup-platform-cicd-nickel [
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
] {
|
||||
print-setup-header "Setting up Platform (CI/CD Profile)"
|
||||
print ""
|
||||
print "CI/CD profile: Ephemeral deployment for automated pipelines"
|
||||
print ""
|
||||
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
# Prefer Docker Compose for CI/CD (faster startup)
|
||||
let deployment_mode = if (has-docker-compose) {
|
||||
"docker-compose"
|
||||
} else if (has-kubectl) {
|
||||
"kubernetes"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
if ($deployment_mode == "") {
|
||||
print-setup-error "Docker Compose or Kubernetes required for CI/CD profile"
|
||||
return {
|
||||
success: false
|
||||
error: "Missing required tools"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info $"Using deployment mode: ($deployment_mode)"
|
||||
|
||||
print-setup-info "Generating Nickel platform configuration..."
|
||||
if not (create-platform-config-nickel $base $deployment_mode "cicd") {
|
||||
print-setup-error "Failed to generate Nickel platform config"
|
||||
return {
|
||||
success: false
|
||||
error: "Failed to generate Nickel platform config"
|
||||
}
|
||||
}
|
||||
|
||||
print-setup-info "Validating Nickel configuration..."
|
||||
let validation = (validate-nickel-config $"($base)/platform/deployment.ncl")
|
||||
if not $validation {
|
||||
print-setup-warning "Nickel validation skipped - continuing with setup"
|
||||
}
|
||||
|
||||
# Start services (CI/CD uses longer timeouts for reliability)
|
||||
let start_result = (start-platform-services $deployment_mode --verbose=$verbose)
|
||||
|
||||
{
|
||||
success: $start_result.success
|
||||
profile: "cicd"
|
||||
deployment: $deployment_mode
|
||||
config_base: $base
|
||||
timestamp: (get-timestamp-iso8601)
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# COMPLETE PLATFORM SETUP
|
||||
# ============================================================================
|
||||
|
||||
# Execute complete platform setup
|
||||
# Execute complete platform setup by profile
|
||||
export def setup-platform-complete-by-profile [
|
||||
profile: string = "developer"
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
] {
|
||||
match $profile {
|
||||
"developer" => { setup-platform-developer $config_base --verbose=$verbose }
|
||||
"production" => { setup-platform-production $config_base --verbose=$verbose }
|
||||
"cicd" => { setup-platform-cicd-nickel $config_base --verbose=$verbose }
|
||||
_ => {
|
||||
print-setup-error $"Unknown profile: ($profile)"
|
||||
{
|
||||
success: false
|
||||
error: $"Unknown profile: ($profile)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Execute complete platform setup (backward compatible)
|
||||
export def setup-platform-complete [
|
||||
setup_mode: string = "solo"
|
||||
config_base: string = ""
|
||||
--verbose = false
|
||||
]: nothing -> record {
|
||||
] {
|
||||
let base = (if ($config_base == "") { (get-config-base-path) } else { $config_base })
|
||||
|
||||
match $setup_mode {
|
||||
"solo" => { setup-platform-solo $base --verbose=$verbose }
|
||||
"multiuser" => { setup-platform-multiuser $base --verbose=$verbose }
|
||||
"cicd" => { setup-platform-cicd $base --verbose=$verbose }
|
||||
_ => {
|
||||
print-setup-error $"Unknown setup mode: ($setup_mode)"
|
||||
{
|
||||
success: false
|
||||
error: $"Unknown setup mode: ($setup_mode)"
|
||||
}
|
||||
}
|
||||
# Map legacy modes to profiles (backward compatibility)
|
||||
let profile = match $setup_mode {
|
||||
"solo" => "developer"
|
||||
"developer" => "developer"
|
||||
"multiuser" => "production"
|
||||
"production" => "production"
|
||||
"cicd" => "cicd"
|
||||
_ => "developer"
|
||||
}
|
||||
|
||||
setup-platform-complete-by-profile $profile $base --verbose=$verbose
|
||||
}
|
||||
|
||||
# Print platform services status report
|
||||
export def print-platform-status []: nothing -> nothing {
|
||||
export def print-platform-status [] {
|
||||
let status = (verify-platform-services)
|
||||
|
||||
print ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user