# Provider-Agnostic Middleware # Uses dynamic provider loading and interface-based dispatch # Supports multi-provider infrastructure deployments use ../../../core/nulib/domain/config/accessor.nu * use ../../../core/nulib/domain/providers/registry.nu * use ../../../core/nulib/domain/providers/loader.nu * use ../../../core/nulib/domain/providers/interface.nu * use ../../../core/nulib/primitives/io/logging.nu * use ../../../core/nulib/primitives/io/interface.nu [_ansi _print] # Initialize middleware export def init-middleware []: nothing -> nothing { init-provider-registry let daemon_mode = (($env.PROVISIONING_DAEMON_MODE? | default "false" | into string) == "true") let quiet_middleware = (($env.PROVISIONING_QUIET_MIDDLEWARE? | default "false" | into string) == "true") log-debug "Provider-agnostic middleware initialized" "middleware" } # Check if provider is allowed by profile (safer version) def is_provider_allowed [provider_name: string]: nothing -> bool { # Disabled - causes | complete issues true } def provider_undefined [ server: record ] { let server_prov = ($server | get -o provider | default "") let available_providers = (list-providers --available-only | get name | str join ", ") # This error is misleading - it's called when function dispatch fails, not when provider isn't found # The actual issue is that the provider function call failed log-error $"Provider function returned null for provider ($server_prov)" "middleware" print $"(_ansi blue_bold)($server.hostname)(_ansi reset) with provider " + $"(_ansi green_bold)($server_prov)(_ansi reset) ($server.zone?) " print $"Note: Provider was loaded but function call failed or returned empty result" } # Dynamic provider dispatch helper def dispatch_provider_function [ provider_name: string function_name: string ...args ]: nothing -> any { # Check if provider is allowed if not (is_provider_allowed $provider_name) { log-error $"Provider ($provider_name) blocked by profile" "middleware" return null } # Load provider if not already loaded let provider = (get-provider $provider_name) if ($provider | is-empty) { log-error $"Failed to load provider ($provider_name)" "middleware" return null } # Call provider function call-provider-function $provider_name $function_name ...$args } # === CORE MIDDLEWARE FUNCTIONS === # Query servers (supports multi-provider) export def mw_query_servers [ settings: record find?: string cols?: string --prov: string --serverpos: int ]: nothing -> list { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let str_find = if $find != null { $find } else { "" } let str_cols = if $cols != null { $cols } else { "" } # Filter infrastructure servers: only enabled ones (not_use = false) let enabled_servers = $settings.data.servers | where {|s| ($s.enabled? | default true) and not ($s.not_use? | default false)} | where {|s| $prov == null or $s.provider == $prov} | where {|s| $serverpos == null or ($settings.data.servers | enumerate | where {|it| $it.item == $s} | first.index) == $serverpos} # If no enabled servers match, return empty if ($enabled_servers | length) == 0 { return [] } # Get unique providers from enabled servers let unique_providers = ($enabled_servers | each {|s| $s.provider} | uniq ) # Get hostnames of enabled servers for filtering results let enabled_hostnames = ($enabled_servers | each {|s| $s.hostname}) # Query each provider ONCE and filter by enabled infrastructure hostnames $unique_providers | each { |provider_name| let res = (dispatch_provider_function $provider_name "query_servers" $str_find $str_cols) if $res == null { provider_undefined {provider: $provider_name, hostname: "unknown", zone: ""} [] } else if ($res | length) > 0 { # Filter results to only servers defined in enabled infrastructure let filtered = $res | where {|server| $server.name in $enabled_hostnames } let result = if $str_find != "" { $filtered | find $str_find } else { $filtered } if $str_cols != "" { let field_list = ($str_cols | split row ",") ($result | select ...$field_list) } else { $result } } else { [] } } | flatten } # Server information (provider-agnostic) export def mw_server_info [ server: record check: bool find?: string cols?: string ]: nothing -> record { let str_find = if $find != null { $find } else { "" } let str_cols = if $cols != null { $cols } else { "" } # CRITICAL FIX: Pass $server.hostname (string), not $server (record) # The provider's server_info function expects: server_info [server: string, check: bool, ...] let res = (dispatch_provider_function $server.provider "server_info" $server.hostname $check $str_find $str_cols ) if $res == null { provider_undefined $server {} } else { $res } if ($res | describe | str starts-with "record") and $res.hostname? != null { let result = if $str_find != "" { $res | find $str_find } else { $res } let info = if $str_cols != "" { let field_list = ($str_cols | split row ",") ($result | select ...$field_list) } else { $result } # Handle provider-specific private IP formats let priv = if $server.provider == "aws" { ($info | get -o private_ips | default [] | each {|it| ($it | select Description PrivateIpAddress VpcId SubnetId Groups) }) } else { ($info | get -o priv | default []) } let full_info = if ($priv | length) > 0 { ($info | merge { private_ips: $priv }) } else { $info } let out = (get-provisioning-out) if ($out | is-empty) and (is-debug-enabled) { print ($full_info | table -e) } if (not $check) and (is-debug-enabled) { ($full_info | table -e) } $full_info } else { $res } } # Server creation (provider-agnostic with profile checking) export def mw_create_server [ settings: record server: record check: bool error_exit: bool ]: nothing -> bool { # Check if provider is allowed by profile if not (is_provider_allowed $server.provider) { log-error $"Provider ($server.provider) blocked by current profile" "middleware" if $error_exit { exit } else { return false } } let zone = $server.zone? | default "" # In check mode, skip provider requirements check (it may be slow or unavailable) # Just report what would be created if $check { _print ($"Check (_ansi cyan_bold)($server.hostname)(_ansi reset) with provider " + $"(_ansi green_bold)($server.provider)(_ansi reset) ($zone) ") return true } # Dynamic provider dispatch for requirements check (only for actual creation) let res = (dispatch_provider_function $server.provider "check_server_requirements" $settings $server $check) if $res == null { provider_undefined $server if $error_exit { exit } else { return false } } if not $res { log-error $"($server.provider) check requirements error for server ($server.hostname)" "middleware" return false } # Provider-specific message (most providers don't implement this, so we skip it) # If needed in future, provider can override the default "Create..." message below _print ($"Create (_ansi blue_bold)($server.hostname)(_ansi reset) with provider " + $"(_ansi green_bold)($server.provider)(_ansi reset) ($zone) ") return true } # Server state management (provider-agnostic) export def mw_server_state [ server: record new_state: string error_exit: bool wait: bool settings: record ]: nothing -> bool { # Check if provider is allowed by profile if not (is_provider_allowed $server.provider) { log-error $"Provider ($server.provider) blocked by current profile" "middleware" if $error_exit { exit } else { return false } } let res = (dispatch_provider_function $server.provider "server_state" $server $new_state $error_exit $wait $settings ) if $res == null { provider_undefined $server if $error_exit { exit } else { return false } } else { let result = $res $result != null } } # Server existence check (provider-agnostic) export def mw_server_exists [ server: record error_exit: bool ]: nothing -> bool { let hostname = $server.hostname let res = (dispatch_provider_function $server.provider "server_exists" $hostname $error_exit) if $res == null { provider_undefined $server if $error_exit { exit } else { false } } else { $res | default false } } # Server running status (provider-agnostic) export def mw_server_is_running [ server: record error_exit: bool ]: nothing -> bool { let hostname = $server.hostname let res = (dispatch_provider_function $server.provider "server_is_running" $hostname $error_exit) if $res == null { provider_undefined $server if $error_exit { exit } else { false } } else { $res | default false } } # Resolve template variables in ip_type (e.g. "{{network_public_ip}}" -> public) def resolve_ip_template [ server: record ip_type: string ]: nothing -> string { # Remove surrounding quotes if present let cleaned = ($ip_type | str trim --char "\"") # Check if ip_type contains template variable {{...}} if ($cleaned | str contains "{{") and ($cleaned | str contains "}}") { # Extract field name between {{ and }} let without_prefix = ($cleaned | str replace "{{" "") let field_name = ($without_prefix | str replace "}}" "") # Return the value of that field from server, or "public" as fallback ($server | get -o $field_name | default "public") } else { # Not a template variable, return as-is $cleaned } } # Get IP (provider-agnostic) export def mw_get_ip [ settings: record server: record ip_type: string error_exit: bool ]: nothing -> string { # Resolve template variables if present let resolved_ip_type = (resolve_ip_template $server $ip_type) let hostname = $server.hostname let res = (dispatch_provider_function $server.provider "get_ip" $settings $hostname $resolved_ip_type $error_exit ) if $res == null { provider_undefined $server if $error_exit { exit } else { "" } } else { let result = $res if $result != null { $result | str trim } else { "" } } } # Multi-provider infrastructure operations export def mw_servers_ips [ settings: record data: list prov?: string serverpos?: int ]: nothing -> list { mut result = [] for srv in ($data | enumerate) { let settings_server = ($settings.data.servers | where {|it| $it.hostname == $srv.item.hostname}) if ($settings_server | length) == 0 { continue } let provider = ($settings_server | get -o 0 | get -o provider | default "") if $prov != null and $provider != $prov { continue } if $serverpos != null and $serverpos != $srv.index { continue } let res = (dispatch_provider_function $provider "servers_ips" $settings [$srv.item] $prov $serverpos ) if $res == null { provider_undefined { provider: $provider, hostname: $srv.item.hostname } } else { let provider_result = $res if $provider_result != null { $result = ($result | append $provider_result) } } } $result } # Multi-provider server deletion export def mw_delete_server [ settings: record server: record keep_storage: bool error_exit: bool ]: nothing -> bool { let zone = $server.zone? | default "" # Show provider-specific server message let msg_result = (dispatch_provider_function $server.provider "on_prov_server" $server ) if ($msg_result != null) { print $msg_result } # Delete server let res = (dispatch_provider_function $server.provider "delete_server" $settings $server $keep_storage $error_exit ) if $res == null { provider_undefined $server if $error_exit { exit } else { false } } else { let result = $res $result != null } } # Load infrastructure servers info (provider-agnostic) export def mw_load_infra_servers_info [ settings: record server: record error_exit: bool ]: nothing -> record { let res = (dispatch_provider_function $server.provider "load_infra_servers_info" $settings $server $error_exit) if $res == null { provider_undefined $server if $error_exit { exit } else { {} } } else { $res } } # Load infrastructure storages info (provider-agnostic) export def mw_load_infra_storages_info [ settings: record server: record error_exit: bool ]: nothing -> record { let res = (dispatch_provider_function $server.provider "load_infra_storages_info" $settings $server $error_exit) if $res == null { provider_undefined $server if $error_exit { exit } else { {} } } else { $res } } # Get infrastructure item for server (provider-agnostic) export def mw_get_infra_item [ server: record settings: record cloud_data: record error_exit: bool ]: nothing -> record { let res = (dispatch_provider_function $server.provider "get_infra_item" $server $settings $cloud_data $error_exit) if $res == null { provider_undefined $server if $error_exit { exit } else { {} } } else { $res } } # Get infrastructure price (provider-agnostic) export def mw_get_infra_price [ server: record data: record key: string error_exit: bool price_col?: string ]: nothing -> any { let res = (dispatch_provider_function $server.provider "get_infra_price" $server $data $key $error_exit $price_col) if $res == null { return (if $error_exit { (exit) } else { 0.0 }) } # Convert result to float (wrapper scripts return strings) let res_type = ($res | describe) if ($res_type | str starts-with "float") or ($res_type | str starts-with "int") { $res } else if ($res_type | str starts-with "string") { # Special handling for "unit" key which returns string like "0.0104 Hrs" if $key == "unit" { $res } else { # Parse string as float (wrapper scripts serialize numbers as strings) # Check if string can be converted to float let float_result = (do --ignore-errors { $res | into float }) if ($float_result | is-empty) { 0.0 } else { $float_result } } } else { # Return 0.0 for any other non-numeric type 0.0 } } # Get all infrastructure prices at once (provider-agnostic) - Phase 2 optimization # Returns: { hour: float, day: float, month: float, unit_info: string } export def mw_get_all_infra_prices [ server: record data: record error_exit: bool price_col?: string ]: nothing -> record { # Try to call provider-specific batched implementation if available let res = (dispatch_provider_function $server.provider "get_all_infra_prices" $server $data $error_exit $price_col) if $res != null { # Provider has batched implementation $res } else { # Fallback to individual calls (for providers without batched implementation) let hour = (mw_get_infra_price $server $data "hour" $error_exit $price_col) { hour: $hour, day: (($hour * 24) | math round -p 4), month: ((mw_get_infra_price $server $data "month" $error_exit $price_col) | math round -p 4), unit_info: (mw_get_infra_price $server $data "unit" $error_exit $price_col) } } } # Get infrastructure storage (provider-agnostic) export def mw_get_infra_storage [ server: record settings: record cloud_data: record error_exit: bool ]: nothing -> record { let res = (dispatch_provider_function $server.provider "get_infra_storage" $server $settings $cloud_data $error_exit) if $res == null { if $error_exit { exit } else { {} } } else { $res } } # === ENHANCED MULTI-PROVIDER OPERATIONS === # Deploy multi-provider infrastructure (simplified) export def mw_deploy_multi_provider_infra [ settings: record deployment_plan: record ]: nothing -> record { log-section "Starting multi-provider deployment" "middleware" # Group servers by provider let provider_groups = ($settings.data.servers | group-by provider) let providers_used = ($provider_groups | columns) log-info $"Will deploy to providers: ($providers_used | str join ', ')" "middleware" # Return basic deployment info { providers_used: $providers_used total_servers: ($settings.data.servers | length) deployment_plan: $deployment_plan status: "planned" timestamp: (date now) } } # Get provider status with capabilities export def mw_provider_status [ --verbose ]: nothing -> table { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let providers = (list-providers --available-only) $providers | each {|provider| let allowed = (is_provider_allowed $provider.name) let capabilities = (get-provider-capabilities-for $provider.name) let basic_info = { name: $provider.name type: ($provider.details? | get -o type | default "extension") available: ($provider.details? | get -o available | default true) loaded: ($provider.details? | get -o loaded | default false) profile_allowed: $allowed status: (if $allowed { "✅ Available" } else { "🛑 Blocked by profile" }) } if $verbose { $basic_info | merge { capabilities: $provider.capabilities server_management: ($capabilities.server_management? | default false) multi_region: ($capabilities.multi_region? | default false) auto_scaling: ($capabilities.auto_scaling? | default false) } } else { $basic_info } } } # Get deployment recommendations for multi-provider setup export def mw_suggest_deployment_strategy [ requirements: record ]: nothing -> record { log-info "Analyzing requirements for deployment strategy" "middleware" let available_providers = (list-providers --available-only) mut recommendations = { strategy: "single-provider" # default primary_provider: "" secondary_providers: [] rationale: [] } # Analyze requirements and suggest strategy if ($requirements.regions? | default [] | length) > 1 { $recommendations.strategy = "multi-provider" $recommendations.rationale = ($recommendations.rationale | append "Multi-region requirement detected") } if ($requirements.high_availability? | default false) { $recommendations.strategy = "multi-provider" $recommendations.rationale = ($recommendations.rationale | append "High availability requirement") } if ($requirements.cost_optimization? | default false) { $recommendations.rationale = ($recommendations.rationale | append "Cost optimization: consider mixing providers") } # Select primary provider based on capabilities let suitable_providers = ($available_providers | where {|p| let caps = (get-provider-capabilities-for $p.name) ($caps.server_management? | default false) == true }) if ($suitable_providers | length) > 0 { $recommendations.primary_provider = ($suitable_providers | get 0 | get name) if $recommendations.strategy == "multi-provider" { $recommendations.secondary_providers = ($suitable_providers | skip 1 | get name) } } log-info $"Recommended strategy: ($recommendations.strategy)" "middleware" $recommendations } # Create cache for server (provider-specific) export def mw_create_cache [ settings: record server: record error_exit: bool ]: nothing -> nothing { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let provider_name = $server.provider # Dynamic provider dispatch let res = (dispatch_provider_function $provider_name "create_cache" $settings $server $error_exit) if $res == null { provider_undefined $server if $error_exit { log-error $"Failed to create cache for ($server.hostname)" "middleware" } } } # Get IP from cache (provider-specific) # server: record with .hostname and .provider fields export def mw_ip_from_cache [ settings: record server: record error_exit: bool ]: nothing -> string { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let provider_name = $server.provider let hostname = $server.hostname let res = (dispatch_provider_function $provider_name "ip_from_cache" $settings $hostname $error_exit) if $res == null { "" } else { $res } } # Modify server (provider-specific) export def mw_modify_server [ settings: record server: record modifications: list check: bool ]: nothing -> any { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let provider_name = $server.provider # Dynamic provider dispatch let res = (dispatch_provider_function $provider_name "modify_server" $settings $server $modifications $check) if $res == null { provider_undefined $server return null } $res } # Post-create server hook (provider-specific) export def mw_post_create_server [ settings: record server: record check: bool ]: nothing -> string { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let provider_name = $server.provider # Dynamic provider dispatch let res = (dispatch_provider_function $provider_name "post_create_server" $settings $server $check) if $res == null { provider_undefined $server return "" } $res } # Enrich template context with provider-cached resources (provider-specific) export def mw_enrich_template_context [ settings: record server: record context: record ]: nothing -> record { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let private_network = ($server.networking?.private_network? | default "") let res = (dispatch_provider_function $server.provider "enrich_template_context" $settings $context $private_network) if $res == null { provider_undefined $server return $context } $res } # Post-generate server hook (provider-specific) export def mw_post_generate_server [ settings: record server: record check: bool ]: nothing -> string { if not ($env.PROVIDER_REGISTRY_INITIALIZED? | default false) { init-middleware | ignore } let provider_name = $server.provider # Dynamic provider dispatch let res = (dispatch_provider_function $provider_name "post_generate_server" $settings $server $check) if $res == null { provider_undefined $server return "" } $res } # === DNS MIDDLEWARE FUNCTIONS === # Provider-agnostic DNS dispatch via dispatch_provider_function. # DNS provider is resolved from zone config (separate from compute provider). export def mw_dns_zone_get [ provider: string zone: string ]: nothing -> record { let res = (dispatch_provider_function $provider "dns_zone_get" $zone) if $res == null { error make { msg: $"DNS zone_get failed for ($zone) via ($provider)" } } $res } export def mw_dns_record_list [ provider: string zone: string ]: nothing -> list { let res = (dispatch_provider_function $provider "dns_record_list" $zone) $res | default [] } export def mw_dns_record_create [ provider: string record: record ]: nothing -> record { let res = (dispatch_provider_function $provider "dns_record_create" $record) if $res == null { error make { msg: $"DNS record_create failed via ($provider)" } } $res } export def mw_dns_record_update [ provider: string id: string record: record ]: nothing -> record { let res = (dispatch_provider_function $provider "dns_record_update" $id $record) if $res == null { error make { msg: $"DNS record_update ($id) failed via ($provider)" } } $res } export def mw_dns_record_delete [ provider: string id: string zone: string ]: nothing -> nothing { dispatch_provider_function $provider "dns_record_delete" $id $zone | ignore } export def mw_dns_record_upsert [ provider: string record: record ]: nothing -> record { let res = (dispatch_provider_function $provider "dns_record_upsert" $record) if $res == null { error make { msg: $"DNS record_upsert failed via ($provider)" } } $res } # === REGISTRAR MIDDLEWARE FUNCTIONS === # Provider-agnostic registrar dispatch via dispatch_provider_function. # Manages domain ownership, nameserver delegation, and expiration — separate from DNS record management. # filter.domains scopes results to declared infra domains; empty filter = admin mode (all domains). export def mw_registrar_list_domains [ provider: string filter: record ]: nothing -> list { let res = (dispatch_provider_function $provider "registrar_list_domains" $filter) $res | default [] } export def mw_registrar_domain_info [ provider: string domain: string ]: nothing -> record { let res = (dispatch_provider_function $provider "registrar_domain_info" $domain) if $res == null { error make { msg: $"Registrar domain_info failed for ($domain) via ($provider)" } } $res } export def mw_registrar_get_nameservers [ provider: string domain: string ]: nothing -> list { let res = (dispatch_provider_function $provider "registrar_get_nameservers" $domain) $res | default [] } export def mw_registrar_set_nameservers [ provider: string domain: string ns: list ]: nothing -> bool { let res = (dispatch_provider_function $provider "registrar_set_nameservers" $domain $ns) $res | default false } export def mw_registrar_set_auto_renew [ provider: string domain: string enable: bool ]: nothing -> bool { let res = (dispatch_provider_function $provider "registrar_set_auto_renew" $domain $enable) $res | default false } # Initialize middleware when loaded export-env { # This will be set when middleware functions are first called }