use ../lib_provisioning/workspace * use ../lib_provisioning/user/config.nu [get-workspace-path, get-active-workspace-details] use ../../../extensions/providers/hetzner/nulib/hetzner/api.nu * use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval, ncl-eval-soft] # Export a Nickel file relative to the workspace root, with provisioning import path. def bootstrap-ncl-export [ws_root: string, rel_path: string]: nothing -> record { let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning") let full_path = ($ws_root | path join $rel_path) let result = (ncl-eval $full_path [$ws_root $prov_root]) $result } # 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}. 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) print $" floating_ip ($fip.name) already exists \(id: ($found.id)\) — skip" return { id: ($found.id | into string), 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) } { id: $fip_id, record: $created } } # 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)" } }