use domain/workspace/mod.nu * use platform/user/config.nu [get-workspace-path, get-active-workspace-details] use ../../../catalog/providers/hetzner/nulib/hetzner/api.nu * use tools/nickel/process.nu [ncl-eval-soft, default-ncl-paths] # Export a Nickel file relative to the workspace root — always bypasses the plugin cache. # Bootstrap modifies live cloud resources; stale config is not acceptable here. def bootstrap-ncl-export [ws_root: string, rel_path: string]: nothing -> record { let full_path = ($ws_root | path join $rel_path) let import_paths = (default-ncl-paths $ws_root) let import_args = ($import_paths | each {|p| ["--import-path" $p] } | flatten) let r = (do { ^nickel export --format json $full_path ...$import_args } | complete) if $r.exit_code != 0 { error make { msg: $"NCL evaluation failed for ($full_path): ($r.stderr | str trim)" } } $r.stdout | from json } # Ensure the private network exists and all declared subnets are present. # Creates the network if absent; reconciles subnets for existing networks. def bootstrap-network [cfg: record]: nothing -> record { let existing = (hetzner_api_list_networks | where name == $cfg.name) let network = if ($existing | is-not-empty) { print $" network ($cfg.name) already exists — skip" ($existing | first) } else { print $" creating network ($cfg.name) ..." let payload = { name: $cfg.name, ip_range: $cfg.ip_range, subnets: ($cfg.subnets? | default []) } let payload = if ("labels" in ($cfg | columns)) { $payload | insert labels $cfg.labels } else { $payload } let created = (hetzner_api_create_network $payload) let delete_protected = ($cfg | get -o protection.delete | default false) if $delete_protected { print $" enabling delete protection on ($cfg.name) ..." let _action = (hetzner_api_network_change_protection ($created.id | into string) true) } $created } # Reconcile subnets: add any declared subnets that are missing from the network. let declared = ($cfg.subnets? | default []) if ($declared | is-not-empty) { let network_detail = (hetzner_api_network_info ($network.id | into string)) let existing_ranges = ($network_detail.subnets? | default [] | each { |s| $s.ip_range }) for sn in $declared { if not ($existing_ranges | any { |r| $r == $sn.ip_range }) { print $" adding subnet ($sn.ip_range) to ($cfg.name) ..." let _action = (hetzner_api_network_add_subnet ($network.id | into string) $sn) print $" ✓ subnet ($sn.ip_range) added" } else { print $" subnet ($sn.ip_range) already present — skip" } } } $network } # Ensure the SSH key exists in Hetzner Cloud, importing it if absent. Returns the ssh_key record. def bootstrap-ssh-key [cfg: record]: nothing -> record { let existing = (hetzner_api_list_ssh_keys | where name == $cfg.name) if ($existing | is-not-empty) { print $" ssh_key ($cfg.name) already exists — skip" return ($existing | first) } let key_path = ($cfg.public_key_path | str replace "~" $nu.home-dir) if (($key_path | path exists) == false) { error make { msg: $"SSH public key not found at ($key_path)" } } let public_key = (open $key_path | str trim) print $" importing ssh_key ($cfg.name) ..." hetzner_api_create_ssh_key $cfg.name $public_key } # Ensure the firewall exists, creating it if absent. Returns the firewall record. def bootstrap-firewall [cfg: record]: nothing -> record { let existing = (hetzner_api_list_firewalls | where name == $cfg.name) if ($existing | is-not-empty) { print $" firewall ($cfg.name) already exists — skip" return ($existing | first) } print $" creating firewall ($cfg.name) ..." let payload = { name: $cfg.name, rules: $cfg.rules } let payload = if ("labels" in ($cfg | columns)) { $payload | insert labels $cfg.labels } else { $payload } hetzner_api_create_firewall $payload } # Ensure a Floating IP exists, creating it if absent. Returns {id, record}. # Reconciles PTR record and server assignment for both new and existing FIPs. def bootstrap-floating-ip [fip: record]: nothing -> record { let existing = (hetzner_api_list_floating_ips | where name == $fip.name) if ($existing | is-not-empty) { let found = ($existing | first) let fip_id = ($found.id | into string) # Reconcile PTR let has_ptr = ("dns_ptr" in ($fip | columns)) and (($fip.dns_ptr | is-empty) == false) if $has_ptr { let raw_ptr = ($found | get -o dns_ptr | default null) let current_ptr = if $raw_ptr == null { "" } else { let t = ($raw_ptr | describe) if ($t | str starts-with "list") or ($t | str starts-with "table") { $raw_ptr | first | get -o dns_ptr | default "" } else if ($t | str starts-with "record") { $raw_ptr | get -o dns_ptr | default "" } else { $raw_ptr | into string } } if $current_ptr != $fip.dns_ptr { print $" floating_ip ($fip.name) — updating PTR: '($current_ptr)' → '($fip.dns_ptr)'" hetzner_api_floating_ip_set_rdns $fip_id $found.ip $fip.dns_ptr | ignore } else { print $" floating_ip ($fip.name) already exists \(id: ($fip_id)\) — PTR ok" } } else { print $" floating_ip ($fip.name) already exists \(id: ($fip_id)\) — skip" } # Reconcile server assignment bootstrap-floating-ip-assign $fip_id $fip $found return { id: $fip_id, record: $found } } print $" creating floating_ip ($fip.name) ..." let description = ($fip | get -o description | default "") let labels = ($fip | get -o labels | default {}) let payload = { type: $fip.type, home_location: ($fip.location? | default ($fip.home_location? | default "")), name: $fip.name, description: $description, labels: $labels, } let created = (hetzner_api_create_floating_ip $payload) let fip_id = ($created.id | into string) let has_ptr = ("dns_ptr" in ($fip | columns)) and (($fip.dns_ptr | is-empty) == false) if $has_ptr { print $" setting PTR ($fip.dns_ptr) for ($created.ip) ..." let _action = (hetzner_api_floating_ip_set_rdns $fip_id $created.ip $fip.dns_ptr) } let delete_protected = ($fip | get -o protection.delete | default false) if $delete_protected { print $" enabling delete protection on ($fip.name) ..." let _action = (hetzner_api_floating_ip_change_protection $fip_id true) } bootstrap-floating-ip-assign $fip_id $fip $created { id: $fip_id, record: $created } } # Reconcile FIP server assignment. # Only acts on pinned FIPs (assignment.mode = 'pinned). Floating FIPs are managed manually or by HCCM. def bootstrap-floating-ip-assign [fip_id: string, fip: record, found: record]: nothing -> nothing { let mode = ($fip | get -o assignment.mode | default null) if $mode != "pinned" { if $mode == "floating" { print $" floating_ip ($fip.name) — floating mode, skipping auto-assign" } return } let target = ($fip | get -o assignment.node | default "") if ($target | is-empty) { print $" WARNING: ($fip.name) is pinned but assignment.node is empty — skipping" return } let servers = (hetzner_api_list_servers | where name == $target) if ($servers | is-empty) { print $" WARNING: pinned server '($target)' not found — skipping FIP assignment" return } let server_id = ($servers | first | get id | into string) let current_server_id = do -i { $found | get -o server.id | default null | into string } | default "" if $current_server_id == $server_id { print $" floating_ip ($fip.name) — already assigned to ($target)" return } # Auto-correct: FIP is on wrong server — re-assign to pinned node if ($current_server_id | is-not-empty) and $current_server_id != "null" { let wrong_host = (hetzner_api_list_servers | where {|s| ($s.id | into string) == $current_server_id} | first | get -o name | default $current_server_id) print $" ⚠ ($fip.name) currently on '($wrong_host)' — auto-correcting to pinned node '($target)'" } print $" floating_ip ($fip.name) — assigning to ($target) \(id: ($server_id)\) ..." let _action = (hetzner_api_assign_floating_ip $fip_id $server_id) print $" ✓ assigned ($fip.name) → ($target)" } # Persist bootstrap resource IDs to .provisioning-state.json in the workspace root. def bootstrap-persist-state [ws_root: string, state: record]: nothing -> nothing { let state_path = ($ws_root | path join ".provisioning-state.json") let existing = if ($state_path | path exists) { open --raw $state_path | from json } else { {} } ($existing | merge $state) | to json --indent 2 | save --force $state_path print $" state written to .provisioning-state.json" } # Provision L1 Hetzner resources: private network, SSH key, firewall, Floating IPs. # # Reads infra/bootstrap.ncl from the workspace root. All operations are idempotent — # existing resources are detected via API list calls and skipped. Resource IDs are # persisted to .provisioning-state.json for use by downstream L2 provisioning. export def "main bootstrap" [ --workspace (-w): string # Workspace name (default: active workspace) --dry-run (-n) # Print what would be created without calling the API ] : nothing -> nothing { # Resolve workspace: explicit flag > PWD config/provisioning.ncl > convention > active let ws_name = if ($workspace | is-not-empty) { $workspace } else { # Priority 1: config/provisioning.ncl in PWD (workspace root detection) let pwd_config = ($env.PWD | path join "config" "provisioning.ncl") let from_pwd = if ($pwd_config | path exists) { let cfg = (ncl-eval-soft $pwd_config [] null) if $cfg != null { $cfg | get -o workspace | default "" } else { "" } } else { "" } if ($from_pwd | is-not-empty) { $from_pwd } else { # Priority 2: convention — directory name = workspace name let convention = ($env.PWD | path basename) let convention_bootstrap = ($env.PWD | path join "infra" "bootstrap.ncl") if ($convention_bootstrap | path exists) { $convention } else { # Priority 3: active workspace let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Use --workspace or run from a workspace directory." } } $details.name } } } # Resolve workspace root: registered path > PWD (when inferred from PWD) let ws_root_registered = do -i { get-workspace-path $ws_name } | default "" let ws_root = if ($ws_root_registered | is-not-empty) { $ws_root_registered } else { # If not registered, we must be in the workspace root (PWD detection above) $env.PWD } let bootstrap_path = ($ws_root | path join "infra/bootstrap.ncl") if (($bootstrap_path | path exists) == false) { error make { msg: $"infra/bootstrap.ncl not found in workspace ($ws_name) at ($ws_root)" } } print $"Bootstrap L1 resources for workspace: ($ws_name)" print $" config: ($bootstrap_path)" let cfg = (bootstrap-ncl-export $ws_root "infra/bootstrap.ncl") # Support both singular `network` and plural `networks` in bootstrap.ncl. let all_networks = if ("networks" in ($cfg | columns)) { $cfg.networks } else { [$cfg.network] } if $dry_run { print "DRY RUN — resources that would be created:" for net in $all_networks { print $" network: ($net.name) \(($net.ip_range)\)" for sn in ($net.subnets? | default []) { print $" subnet: ($sn.ip_range) \(($sn.type), ($sn.network_zone)\)" } } print $" ssh_key: ($cfg.ssh_key.name)" print $" firewall: ($cfg.firewall.name)" for rule in $cfg.firewall.rules { let port_str = if ($rule.port | is-empty) or ($rule.port == null) { "any" } else { $rule.port } let src = ($rule.source_ips | str join ", ") print $" ($rule.direction) ($rule.protocol)/($port_str) ← ($src)" } for fip in $cfg.floating_ips { print $" floating_ip: ($fip.name) \(($fip.type), ($fip.home_location)\)" } return } print "\n[networks]" let network_results = ($all_networks | each { |net| bootstrap-network $net }) # Primary network is the first one (used for state persistence) let network = ($network_results | first) print "\n[ssh_key]" let ssh_key = (bootstrap-ssh-key $cfg.ssh_key) print "\n[firewall]" let firewall = (bootstrap-firewall $cfg.firewall) print "\n[floating_ips]" let fip_results = ($cfg.floating_ips | each {|fip| bootstrap-floating-ip $fip }) let fip_state = ($fip_results | reduce --fold {} {|entry, acc| let key = ($entry.record.name | str replace --all "librecloud-fip-" "" | str replace --all "-" "_") $acc | insert $key { id: $entry.id, ip: $entry.record.ip, name: $entry.record.name } }) bootstrap-persist-state $ws_root { bootstrap: { network_id: ($network.id | into string), network_name: $network.name, ssh_key_id: ($ssh_key.id | into string), firewall_id: ($firewall.id | into string), floating_ips: $fip_state, } } # Trigger reconcile so SurrealDB resource records reflect the just-bootstrapped state. # Best-effort: silently skipped if the orchestrator daemon is not running. let orchestrator_url = ($env.ORCHESTRATOR_URL? | default "http://localhost:8080") do -i { http post $"($orchestrator_url)/api/v1/infra/reconcile" {workspace: $ws_name} | ignore } print "\nBootstrap complete." print $" network: ($network.name) id=($network.id) range=($cfg.network.ip_range)" for sn in $cfg.network.subnets { print $" subnet: ($sn.ip_range) \(($sn.type), ($sn.network_zone)\)" } print $" firewall: ($firewall.name) id=($firewall.id) rules=($cfg.firewall.rules | length)" for fip in $fip_results { print $" fip ($fip.record.name): id=($fip.id) ip=($fip.record.ip)" } } # Sync firewall rules from infra/bootstrap.ncl to Hetzner — replaces all rules idempotently. # Safe to run at any time; the set_rules API is a full replacement (not incremental). export def "main bootstrap sync-firewall" [ --workspace (-w): string # Workspace name (default: active workspace) --dry-run (-n) # Print rules without calling the API ]: nothing -> nothing { let ws_name = if ($workspace | is-not-empty) { $workspace } else { let pwd_config = ($env.PWD | path join "config" "provisioning.ncl") let from_pwd = if ($pwd_config | path exists) { let cfg = (ncl-eval-soft $pwd_config [] null) if $cfg != null { $cfg | get -o workspace | default "" } else { "" } } else { "" } if ($from_pwd | is-not-empty) { $from_pwd } else { let convention = ($env.PWD | path basename) if (($env.PWD | path join "infra" "bootstrap.ncl") | path exists) { $convention } else { let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Use --workspace or run from a workspace directory." } } $details.name } } } let ws_root_registered = do -i { get-workspace-path $ws_name } | default "" let ws_root = if ($ws_root_registered | is-not-empty) { $ws_root_registered } else { $env.PWD } let bootstrap_path = ($ws_root | path join "infra/bootstrap.ncl") if (($bootstrap_path | path exists) == false) { error make { msg: $"infra/bootstrap.ncl not found in workspace ($ws_name) at ($ws_root)" } } let cfg = (bootstrap-ncl-export $ws_root "infra/bootstrap.ncl") let fw_cfg = $cfg.firewall let declared_rules = $fw_cfg.rules let existing = (hetzner_api_list_firewalls | where name == $fw_cfg.name) if ($existing | is-empty) { error make { msg: $"Firewall '($fw_cfg.name)' not found — run 'prvng bootstrap' first to create it" } } let fw_id = ($existing | first | get id | into string) let _fmt_rule = {|rule| let port_str = if $rule.port == null { "any" } else { $rule.port } let src = ($rule.source_ips | str join ", ") $"($rule.direction) ($rule.protocol)/($port_str) ← ($src)" } if $dry_run { print $"DRY RUN — rules to set on ($fw_cfg.name) \(id: ($fw_id)\):" for rule in $declared_rules { print $" (do $_fmt_rule $rule)" } return } print $"Syncing firewall: ($fw_cfg.name) \(id: ($fw_id)\)" let _result = (hetzner_api_set_firewall_rules $fw_id $declared_rules) print $" ✓ ($declared_rules | length) rules applied" for rule in $declared_rules { print $" (do $_fmt_rule $rule)" } } export def "main bootstrap sync-alias-ips" [ --workspace (-w): string # Workspace name (default: active workspace) --dry-run (-n) # Print changes without calling the API ]: nothing -> nothing { let ws_name = if ($workspace | is-not-empty) { $workspace } else { let pwd_config = ($env.PWD | path join "config" "provisioning.ncl") let from_pwd = if ($pwd_config | path exists) { let cfg = (ncl-eval-soft $pwd_config [] null) if $cfg != null { $cfg | get -o workspace | default "" } else { "" } } else { "" } if ($from_pwd | is-not-empty) { $from_pwd } else { let convention = ($env.PWD | path basename) if (($env.PWD | path join "infra" "bootstrap.ncl") | path exists) { $convention } else { let details = (get-active-workspace-details) if ($details == null) { error make { msg: "No active workspace. Use --workspace or run from a workspace directory." } } $details.name } } } let ws_root_registered = do -i { get-workspace-path $ws_name } | default "" let ws_root = if ($ws_root_registered | is-not-empty) { $ws_root_registered } else { $env.PWD } let bootstrap_path = ($ws_root | path join "infra/bootstrap.ncl") if (($bootstrap_path | path exists) == false) { error make { msg: $"infra/bootstrap.ncl not found in workspace ($ws_name) at ($ws_root)" } } let cfg = (bootstrap-ncl-export $ws_root "infra/bootstrap.ncl") if not ($cfg | columns | any { $in == "alias_ips" }) { print $"No alias_ips declared in bootstrap.ncl for workspace ($ws_name) — nothing to do." return } let alias_cfg = $cfg.alias_ips if $dry_run { print $"DRY RUN — alias IPs to set for workspace ($ws_name):" for entry in ($alias_cfg | transpose server ips) { print $" ($entry.server): ($entry.ips | str join ', ')" } return } print $"Syncing alias IPs for workspace ($ws_name):" for entry in ($alias_cfg | transpose server ips) { let alias_list = ($entry.ips | str join ",") print $" ($entry.server) → ($alias_list)" let result = (do -i { ^hcloud server change-alias-ips $entry.server --network ($cfg.network.name) --alias-ips $alias_list } | complete) if $result.exit_code != 0 { error make { msg: $"Failed to set alias IPs on ($entry.server): ($result.stderr | str trim)" } } print $" ✓ applied" } }