provisioning-core/nulib/orchestration/bootstrap.nu

478 lines
20 KiB
Text
Raw Permalink Normal View History

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"
}
}