#!/usr/bin/env nu # Cloudflare DNS reconcile — runs on the provisioning machine with native http/json. # Shared by every cluster component that publishes an A record for its gateway IP. # The cluster nodes never touch Cloudflare and carry no JSON tooling; the whole # reconcile lives here, on provisioning, where nushell parses responses natively. # Derive the apex (registrable) zone from a fully-qualified domain when the # component does not declare one explicitly: the last two labels. def _apex_zone [domain: string]: nothing -> string { $domain | split row "." | last 2 | str join "." } # Upsert a single A record to point $domain at $target_ip inside $zone. # Idempotent: skips when the record already resolves to the target. Never fails # the deploy — a DNS hiccup is reported as a warning, not a hard error. export def cf-dns-upsert [ cf_token: string # Cloudflare API token scoped to the zone domain: string # FQDN to publish (e.g. git.example.com) target_ip: string # A-record value (the gateway floating IP) zone: string = "" # registrable zone; derived from domain when empty ]: nothing -> nothing { if ($cf_token | is-empty) { print $" [warn] DNS: no Cloudflare token for ($domain) — skipping" return } if ($domain | is-empty) or ($target_ip | is-empty) { print " [skip] DNS: domain or target_ip empty — skipping" return } let zone = if ($zone | is-not-empty) { $zone } else { _apex_zone $domain } let base = "https://api.cloudflare.com/client/v4" let hdr = { Authorization: $"Bearer ($cf_token)" } let zone_resp = (do -i { http get --headers $hdr $"($base)/zones?name=($zone)&status=active" } | default {}) let zone_id = ($zone_resp | get -o result.0.id | default "") if ($zone_id | is-empty) { print $" [warn] DNS: zone '($zone)' not found in Cloudflare" return } let rec_resp = (do -i { http get --headers $hdr $"($base)/zones/($zone_id)/dns_records?name=($domain)&type=A" } | default {}) let record_id = ($rec_resp | get -o result.0.id | default "") let current_ip = ($rec_resp | get -o result.0.content | default "") if ($record_id | is-not-empty) and ($current_ip == $target_ip) { print $" [ok] DNS: ($domain) → ($target_ip) \(already correct\)" return } let payload = { type: "A", name: $domain, content: $target_ip, ttl: 300, proxied: false } let resp = if ($record_id | is-not-empty) { do -i { http put --headers $hdr --content-type application/json $"($base)/zones/($zone_id)/dns_records/($record_id)" $payload } | default {} } else { do -i { http post --headers $hdr --content-type application/json $"($base)/zones/($zone_id)/dns_records" $payload } | default {} } if (($resp | get -o success | default false) == true) { let from = if ($current_ip | is-empty) { "none" } else { $current_ip } print $" [ok] DNS: ($domain) → ($target_ip) \(updated from ($from)\)" } else { let err = ($resp | get -o errors.0.message | default "unknown") print $" [warn] DNS update failed for ($domain): ($err) \(continuing\)" } } # Verify the A record for $domain actually resolves to $target_ip in Cloudflare. # Read-only (no writes). Returns true ONLY when the record exists AND its content # equals target_ip. This is the safety net for cf-dns-upsert, which is do -i # throughout and never returns an error — the caller must confirm the outcome. export def cf-dns-verify [ cf_token: string domain: string target_ip: string zone: string = "" ]: nothing -> bool { if ($cf_token | is-empty) or ($domain | is-empty) or ($target_ip | is-empty) { return false } let zone = if ($zone | is-not-empty) { $zone } else { _apex_zone $domain } let base = "https://api.cloudflare.com/client/v4" let hdr = { Authorization: $"Bearer ($cf_token)" } let zone_resp = (do -i { http get --headers $hdr $"($base)/zones?name=($zone)&status=active" } | default {}) let zone_id = ($zone_resp | get -o result.0.id | default "") if ($zone_id | is-empty) { return false } let rec_resp = (do -i { http get --headers $hdr $"($base)/zones/($zone_id)/dns_records?name=($domain)&type=A" } | default {}) ($rec_resp | get -o result.0.content | default "") == $target_ip } # True when an A record for $domain currently exists in Cloudflare (any value). # Existence probe for the delete-phase postcondition (record must be ABSENT after # cf-dns-delete) — distinct from cf-dns-verify, which checks value equality. export def cf-dns-exists [ cf_token: string domain: string zone: string = "" ]: nothing -> bool { if ($cf_token | is-empty) or ($domain | is-empty) { return false } let zone = if ($zone | is-not-empty) { $zone } else { _apex_zone $domain } let base = "https://api.cloudflare.com/client/v4" let hdr = { Authorization: $"Bearer ($cf_token)" } let zone_resp = (do -i { http get --headers $hdr $"($base)/zones?name=($zone)&status=active" } | default {}) let zone_id = ($zone_resp | get -o result.0.id | default "") if ($zone_id | is-empty) { return false } let rec_resp = (do -i { http get --headers $hdr $"($base)/zones/($zone_id)/dns_records?name=($domain)&type=A" } | default {}) ($rec_resp | get -o result.0.id | default "") | is-not-empty } # Delete the A record for $domain in Cloudflare. Idempotent: an already-absent # record is a no-op success. The inverse of cf-dns-upsert for the op delete phase # (ADR-051 delete-inverts-cleanup) — so a deleted component leaves no orphaned A # record. Runs on the provisioner; the token never reaches the nodes. export def cf-dns-delete [ cf_token: string domain: string zone: string = "" ]: nothing -> nothing { if ($cf_token | is-empty) { print $" [warn] DNS: no Cloudflare token for ($domain) — skipping delete" return } if ($domain | is-empty) { print " [skip] DNS: domain empty — skipping delete" return } let zone = if ($zone | is-not-empty) { $zone } else { _apex_zone $domain } let base = "https://api.cloudflare.com/client/v4" let hdr = { Authorization: $"Bearer ($cf_token)" } let zone_resp = (do -i { http get --headers $hdr $"($base)/zones?name=($zone)&status=active" } | default {}) let zone_id = ($zone_resp | get -o result.0.id | default "") if ($zone_id | is-empty) { print $" [warn] DNS: zone '($zone)' not found in Cloudflare" return } let rec_resp = (do -i { http get --headers $hdr $"($base)/zones/($zone_id)/dns_records?name=($domain)&type=A" } | default {}) let record_id = ($rec_resp | get -o result.0.id | default "") if ($record_id | is-empty) { print $" [ok] DNS: ($domain) already absent \(nothing to delete\)" return } let resp = (do -i { http delete --headers $hdr $"($base)/zones/($zone_id)/dns_records/($record_id)" } | default {}) if (($resp | get -o success | default false) == true) { print $" [ok] DNS: ($domain) A record deleted" } else { let err = ($resp | get -o errors.0.message | default "unknown") print $" [warn] DNS delete failed for ($domain): ($err) \(continuing\)" } } # Upsert many A records — for multi-site components (e.g. rev_proxy, one record # per proxied site, all pointing at the shared FIP). Reads a JSON file of # [{token, domain, target_ip, zone}] so per-zone tokens never land in argv. export def cf-dns-upsert-many [records_file: string]: nothing -> nothing { if ($records_file | is-empty) or (not ($records_file | path exists)) { print " [skip] DNS: no records file — skipping" return } let records = (do -i { open $records_file } | default []) for r in $records { cf-dns-upsert ( $r | get -o token | default "" ) ( $r | get -o domain | default "" ) ( $r | get -o target_ip | default "" ) ( $r | get -o zone | default "" ) } } # CLI entry — invoked as `nu cloudflare-dns.nu [zone]` with # the token supplied out-of-band via $env.CF_API_TOKEN so it never lands in argv # (and thus never in `ps`). Used by the provisioning orchestrator post-install. def main [ domain: string target_ip: string zone: string = "" ]: nothing -> nothing { let token = ($env.CF_API_TOKEN? | default "") cf-dns-upsert $token $domain $target_ip $zone # Verification check — cf-dns-upsert is do -i throughout and returns no error, # so a failed publish is otherwise silent. Confirm the record actually landed # and make a miss LOUD. exit 1 is swallowed by the caller's `do -i` (deploy is # not broken), but the [ERROR] line surfaces and the non-zero is observable to # any caller that switches from `do -i` to `complete`. if ($domain | is-not-empty) and ($target_ip | is-not-empty) { if (cf-dns-verify $token $domain $target_ip $zone) { print $" [ok] DNS verified: ($domain) → ($target_ip)" } else { print $" [ERROR] DNS NOT confirmed: ($domain) → ($target_ip) absent after reconcile — publish manually or re-run" exit 1 } } } # `nu cloudflare-dns.nu many ` — multi-site entry. def "main many" [records_file: string]: nothing -> nothing { cf-dns-upsert-many $records_file } # `nu cloudflare-dns.nu delete [zone]` — delete-phase entry. Removes the # A record and confirms its absence, exiting non-zero if it survives (so the # caller's `complete` surfaces an unconfirmed delete). Inverse of `main`. def "main delete" [domain: string, zone: string = ""]: nothing -> nothing { let token = ($env.CF_API_TOKEN? | default "") cf-dns-delete $token $domain $zone if ($domain | is-not-empty) { if (cf-dns-exists $token $domain $zone) { print $" [ERROR] DNS delete NOT confirmed: ($domain) still present after delete — remove it manually" exit 1 } else { print $" [ok] DNS delete verified: ($domain) absent" } } }