# 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 [--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 [--version ]" 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 [--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 [--format ]" 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 [--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 ](_ansi reset) List all available providers Formats: table (default value), json, yaml (_ansi blue)provisioning providers info [--nickel](_ansi reset) Show detailed provider information with optional Nickel details (_ansi green_bold)[Provider Installation](_ansi reset) (_ansi blue)provisioning providers install [--version ](_ansi reset) Install provider for an infrastructure Default version: 0.0.1 (_ansi blue)provisioning providers remove [--force](_ansi reset) Remove provider from infrastructure --force skips confirmation prompt (_ansi blue)provisioning providers installed [--format ](_ansi reset) List installed providers for infrastructure Formats: table (default value), json, yaml (_ansi blue)provisioning providers validate (_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) " }