provisioning-core/nulib/cli/handlers/utilities/providers.nu

491 lines
16 KiB
Text

# Provider Command Handlers
# Domain: Provider discovery, installation, removal, validation, and information
# REMOVED: use ../../../lib_provisioning * - causes circular import
use cli/flags.nu *
use primitives/io/interface.nu [_print _ansi]
# Validate identifier is safe from path/command injection
def validate_safe_identifier [id: string] {
# Returns true if INVALID (contains dangerous patterns)
let has_slash = ($id | str contains "/")
let has_dotdot = ($id | str contains "..")
let starts_slash = ($id | str starts-with "/")
let has_semicolon = ($id | str contains ";")
let has_pipe = ($id | str contains "|")
let has_ampersand = ($id | str contains "&")
let has_dollar = ($id | str contains "$")
let has_backtick = ($id | str contains "`")
if $has_slash or $has_dotdot or $starts_slash or $has_semicolon or $has_pipe or $has_ampersand or $has_dollar or $has_backtick {
return true
}
false
}
# Main providers command handler - Manage infrastructure providers
export def handle_providers [ops: string, flags: record] {
# Parse subcommand and arguments
let parts = if ($ops | is-not-empty) {
($ops | str trim | split row " " | where { |x| ($x | is-not-empty) })
} else {
[]
}
let subcommand = if ($parts | length) > 0 { $parts | get 0 } else { "list" }
let args = if ($parts | length) > 1 { $parts | skip 1 } else { [] }
match $subcommand {
"list" => { handle_providers_list $flags $args }
"info" => { handle_providers_info $args $flags }
"install" => { handle_providers_install $args $flags }
"remove" => { handle_providers_remove $args $flags }
"installed" => { handle_providers_installed $args $flags }
"validate" => { handle_providers_validate $args $flags }
"help" | "-h" | "--help" => { show_providers_help }
_ => {
print $"❌ Unknown providers subcommand: ($subcommand)"
print ""
show_providers_help
exit 1
}
}
}
# List all available providers
def handle_providers_list [flags: record, args: list] {
_print $"(_ansi green)PROVIDERS(_ansi reset) list: \n"
# Parse flags
let show_nickel = ($args | any { |x| $x == "--nickel" })
let format_idx = ($args | enumerate | where item == "--format" | get 0?.index | default (-1))
let format = if $format_idx >= 0 and ($args | length) > ($format_idx + 1) {
$args | get ($format_idx + 1)
} else {
"table"
}
let no_cache = ($args | any { |x| $x == "--no-cache" })
# Get providers using cached Nickel module loader
let providers = if $no_cache {
(discover-nickel-modules "providers")
} else {
(discover-nickel-modules-cached "providers")
}
match $format {
"json" => {
_print ($providers | to json) "json" "result" "table"
}
"yaml" => {
_print ($providers | to yaml) "yaml" "result" "table"
}
_ => {
# Table format - show summary or full with --nickel
if $show_nickel {
_print ($providers | to json) "json" "result" "table"
} else {
# Show simplified table
let simplified = ($providers | each {|p|
{name: $p.name, type: $p.type, version: $p.version}
})
_print ($simplified | to json) "json" "result" "table"
}
}
}
}
# Show detailed provider information
def handle_providers_info [args: list, flags: record] {
if ($args | is-empty) {
print "❌ Provider name required"
print "Usage: provisioning providers info <provider> [--nickel] [--no-cache]"
exit 1
}
let provider_name = $args | get 0
# Validate provider name
if (validate_safe_identifier $provider_name) {
error make { msg: "Invalid provider name - contains invalid characters" }
}
let show_nickel = ($args | any { |x| $x == "--nickel" })
let no_cache = ($args | any { |x| $x == "--no-cache" })
print $"(_ansi blue_bold)📋 Provider Information: ($provider_name)(_ansi reset)"
print ""
let providers = if $no_cache {
(discover-nickel-modules "providers")
} else {
(discover-nickel-modules-cached "providers")
}
let provider_info = ($providers | where name == $provider_name)
if ($provider_info | is-empty) {
print $"❌ Provider not found: ($provider_name)"
exit 1
}
let info = ($provider_info | first)
print $" Name: ($info.name)"
print $" Type: ($info.type)"
print $" Path: ($info.path)"
print $" Has Nickel: ($info.has_nickel)"
if $show_nickel and $info.has_nickel {
print ""
print " (_ansi cyan_bold)Nickel Module:(_ansi reset)"
print $" Module Name: ($info.module_name)"
print $" Nickel Path: ($info.schema_path)"
print $" Version: ($info.version)"
print $" Edition: ($info.edition)"
# Check for nickel.mod file
let decl_mod = ($info.schema_path | path join "nickel.mod")
if ($decl_mod | path exists) {
print ""
print $" (_ansi cyan_bold)nickel.mod content:(_ansi reset)"
open $decl_mod | lines | each {|line| print $" ($line)"}
}
}
print ""
}
# Install provider for infrastructure
def handle_providers_install [args: list, flags: record] {
if ($args | length) < 2 {
print "❌ Provider name and infrastructure required"
print "Usage: provisioning providers install <provider> <infra> [--version <v>]"
exit 1
}
let provider_name = $args | get 0
let infra_name = $args | get 1
# Validate provider and infrastructure names
if (validate_safe_identifier $provider_name) {
error make { msg: "Invalid provider name - contains invalid characters" }
}
if (validate_safe_identifier $infra_name) {
error make { msg: "Invalid infrastructure name - contains invalid characters" }
}
# Extract version flag if present
let version_idx = ($args | enumerate | where item == "--version" | get 0?.index | default (-1))
let version = if $version_idx >= 0 and ($args | length) > ($version_idx + 1) {
$args | get ($version_idx + 1)
} else {
"0.0.1"
}
# Resolve infrastructure path
let infra_path = (resolve_infra_path $infra_name)
if ($infra_path | is-empty) {
print $"❌ Infrastructure not found: ($infra_name)"
exit 1
}
# Install provider
install-provider $provider_name $infra_path --version $version
print ""
print $"(_ansi yellow_bold)💡 Next steps:(_ansi reset)"
print $" 1. Check the manifest: ($infra_path)/providers.manifest.yaml"
print $" 2. Update server definitions to use ($provider_name)"
print $" 3. Run: nickel run defs/servers.ncl"
}
# Remove provider from infrastructure
def handle_providers_remove [args: list, flags: record] {
if ($args | length) < 2 {
print "❌ Provider name and infrastructure required"
print "Usage: provisioning providers remove <provider> <infra> [--force]"
exit 1
}
let provider_name = $args | get 0
let infra_name = $args | get 1
# Validate provider and infrastructure names
if (validate_safe_identifier $provider_name) {
error make { msg: "Invalid provider name - contains invalid characters" }
}
if (validate_safe_identifier $infra_name) {
error make { msg: "Invalid infrastructure name - contains invalid characters" }
}
let force = ($args | any { |x| $x == "--force" })
# Resolve infrastructure path
let infra_path = (resolve_infra_path $infra_name)
if ($infra_path | is-empty) {
print $"❌ Infrastructure not found: ($infra_name)"
exit 1
}
# Confirmation unless forced
if not $force {
print $"(_ansi yellow)⚠️ This will remove provider ($provider_name) from ($infra_name)(_ansi reset)"
print " Nickel dependencies will be updated."
let response = (input "Continue? (y/N): ")
if ($response | str downcase) != "y" {
print "❌ Cancelled"
return
}
}
# Remove provider
remove-provider $provider_name $infra_path
}
# List installed providers for infrastructure
def handle_providers_installed [args: list, flags: record] {
if ($args | is-empty) {
print "❌ Infrastructure name required"
print "Usage: provisioning providers installed <infra> [--format <fmt>]"
exit 1
}
let infra_name = $args | get 0
# Validate infrastructure name
if (validate_safe_identifier $infra_name) {
error make { msg: "Invalid infrastructure name - contains invalid characters" }
}
# Parse format flag
let format_idx = ($args | enumerate | where item == "--format" | get 0?.index | default (-1))
let format = if $format_idx >= 0 and ($args | length) > ($format_idx + 1) {
$args | get ($format_idx + 1)
} else {
"table"
}
# Resolve infrastructure path
let infra_path = (resolve_infra_path $infra_name)
if ($infra_path | is-empty) {
print $"❌ Infrastructure not found: ($infra_name)"
exit 1
}
let manifest_path = ($infra_path | path join "providers.manifest.yaml")
if not ($manifest_path | path exists) {
print $"❌ No providers.manifest.yaml found in ($infra_name)"
exit 1
}
let manifest = (open $manifest_path)
let providers = if ($manifest | get providers? | is-not-empty) {
$manifest | get providers
} else if ($manifest | get loaded_providers? | is-not-empty) {
$manifest | get loaded_providers
} else {
[]
}
print $"(_ansi blue_bold)📦 Installed providers for ($infra_name):(_ansi reset)"
print ""
match $format {
"json" => {
_print ($providers | to json) "json" "result" "table"
}
"yaml" => {
_print ($providers | to yaml) "yaml" "result" "table"
}
_ => {
_print ($providers | to json) "json" "result" "table"
}
}
}
# Validate provider installation
def handle_providers_validate [args: list, flags: record] {
if ($args | is-empty) {
print "❌ Infrastructure name required"
print "Usage: provisioning providers validate <infra> [--no-cache]"
exit 1
}
let infra_name = $args | get 0
# Validate infrastructure name
if (validate_safe_identifier $infra_name) {
error make { msg: "Invalid infrastructure name - contains invalid characters" }
}
let no_cache = ($args | any { |x| $x == "--no-cache" })
print $"(_ansi blue_bold)🔍 Validating providers for ($infra_name)...(_ansi reset)"
print ""
# Resolve infrastructure path
let infra_path = (resolve_infra_path $infra_name)
if ($infra_path | is-empty) {
print $"❌ Infrastructure not found: ($infra_name)"
exit 1
}
# Refactored from mutable to immutable accumulation (Rule 3)
let validation_result = (
# Check manifest exists
let manifest_path = ($infra_path | path join "providers.manifest.yaml");
let initial = {has_manifest: false, errors: []};
if not ($manifest_path | path exists) {
$initial | upsert has_manifest false | upsert errors ["providers.manifest.yaml not found"]
} else {
# Check each provider in manifest
let manifest = (open $manifest_path)
let providers = ($manifest | get providers? | default [])
# Load providers once using cache
let all_providers = if $no_cache {
(discover-nickel-modules "providers")
} else {
(discover-nickel-modules-cached "providers")
}
# Use reduce --fold to accumulate validation errors (Rule 3)
let validation = ($providers | reduce --fold {errors: []} {|provider, result|
print $" Checking ($provider.name)..."
# Check if provider exists in cached list
let available = ($all_providers | where name == $provider.name)
if ($available | is-empty) {
$result | upsert errors ($result.errors | append $"Provider not found: ($provider.name)")
print $" ❌ Not found in extensions"
} else {
let provider_info = ($available | first)
# Check if symlink exists
let modules_dir = ($infra_path | path join ".nickel-modules")
let link_path = ($modules_dir | path join $provider_info.module_name)
if not ($link_path | path exists) {
$result | upsert errors ($result.errors | append $"Symlink missing: ($link_path)")
print $" ❌ Symlink not found"
} else {
print $" ✓ OK"
$result
}
}
})
# Check nickel.mod
let nickel_mod_path = ($infra_path | path join "nickel.mod")
let final_errors = if not ($nickel_mod_path | path exists) {
($validation.errors | append "nickel.mod not found")
} else {
$validation.errors
}
$initial | upsert has_manifest true | upsert errors $final_errors
}
)
print ""
# Report results
if ($validation_result.errors | is-empty) {
print "(_ansi green)✅ Validation passed - all providers correctly installed(_ansi reset)"
} else {
print "(_ansi red)❌ Validation failed:(_ansi reset)"
$validation_result.errors | each {|error| print $" • ($error)"}
exit 1
}
}
# Helper: Resolve infrastructure path
def resolve_infra_path [infra: string] {
if ($infra | path exists) {
return $infra
}
# Try workspace/infra path
let workspace_path = $"workspace/infra/($infra)"
if ($workspace_path | path exists) {
return $workspace_path
}
# Try absolute workspace path
let proj_root = ($env.PROVISIONING_ROOT? | default ($env.HOME | path join "Development/provisioning"))
let abs_workspace_path = ($proj_root | path join "workspace" "infra" $infra)
if ($abs_workspace_path | path exists) {
return $abs_workspace_path
}
return ""
}
# Show providers help
def show_providers_help [] {
print $"
(_ansi cyan_bold)╔══════════════════════════════════════════════════╗(_ansi reset)
(_ansi cyan_bold)║(_ansi reset) 📦 PROVIDER MANAGEMENT (_ansi cyan_bold)║(_ansi reset)
(_ansi cyan_bold)╚══════════════════════════════════════════════════╝(_ansi reset)
(_ansi green_bold)[Available Providers](_ansi reset)
(_ansi blue)provisioning providers list [--nickel] [--format <fmt>](_ansi reset)
List all available providers
Formats: table (default value), json, yaml
(_ansi blue)provisioning providers info <provider> [--nickel](_ansi reset)
Show detailed provider information with optional Nickel details
(_ansi green_bold)[Provider Installation](_ansi reset)
(_ansi blue)provisioning providers install <provider> <infra> [--version <v>](_ansi reset)
Install provider for an infrastructure
Default version: 0.0.1
(_ansi blue)provisioning providers remove <provider> <infra> [--force](_ansi reset)
Remove provider from infrastructure
--force skips confirmation prompt
(_ansi blue)provisioning providers installed <infra> [--format <fmt>](_ansi reset)
List installed providers for infrastructure
Formats: table (default value), json, yaml
(_ansi blue)provisioning providers validate <infra>(_ansi reset)
Validate provider installation and configuration
(_ansi green_bold)EXAMPLES(_ansi reset)
# List all providers
provisioning providers list
# Show Nickel module details
provisioning providers info upcloud --nickel
# Install provider
provisioning providers install upcloud myinfra
# List installed providers
provisioning providers installed myinfra
# Validate installation
provisioning providers validate myinfra
# Remove provider
provisioning providers remove aws myinfra --force
(_ansi default_dimmed)💡 Use 'provisioning help providers' for more information(_ansi reset)
"
}