provisioning-core/nulib/main_provisioning/dns.nu

833 lines
31 KiB
Text

#!/usr/bin/env nu
# provisioning dns — DNS record management via the configured provider middleware.
use ../../../catalog/providers/prov_lib/middleware.nu [
mw_dns_zone_get
mw_dns_record_list
mw_dns_record_create
mw_dns_record_update
mw_dns_record_delete
mw_dns_record_upsert
]
def load_capabilities [ws_path: string]: nothing -> record {
let cap_file = ($ws_path | path join "infra/libre-wuji/capabilities.ncl")
if not ($cap_file | path exists) { return {} }
let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
let r = (do { ^nickel export --format json --import-path $prov_root $cap_file } | complete)
if $r.exit_code != 0 { return {} }
($r.stdout | from json).provides?.dns? | default {}
}
def load_zones [workspace?: string]: nothing -> record {
let ws_path = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones_file = ($ws_path | path join "infra/lib/dns_zones.ncl")
if not ($zones_file | path exists) {
error make --unspanned { msg: $"dns_zones.ncl not found at ($zones_file)" }
}
let r = (do { ^nickel export --format json $zones_file } | complete)
if $r.exit_code != 0 {
error make --unspanned { msg: $"Failed to evaluate dns_zones.ncl: ($r.stderr)" }
}
($r.stdout | from json).zones
}
def select_zone [zones: record, zone?: string]: nothing -> string {
if ($zone | is-not-empty) { return $zone }
let names = ($zones | columns)
if ($names | length) == 1 { return ($names | first) }
let sel = ($names | input list "Select zone:")
if ($sel | is-empty) {
error make --unspanned { msg: "No zone selected" }
}
$sel
}
def resolve_provider [zones: record, zone: string]: nothing -> string {
let override = ($env | get -o DNS_PROVIDER_OVERRIDE | default "")
if ($override | is-not-empty) { return $override }
let z = ($zones | get -o $zone)
if ($z | is-empty) {
error make --unspanned { msg: $"Zone '($zone)' not found in dns_zones.ncl" }
}
$z.provider | into string
}
def --env set_cf_token [zones: record, zone: string, ws_path: string]: nothing -> nothing {
let override = ($env | get -o DNS_PROVIDER_OVERRIDE | default "")
if ($override | is-not-empty) and $override != "cloudflare" { return }
let z = ($zones | get -o $zone | default {})
let secret_ref = ($z.secret_ref? | default "")
if ($secret_ref | is-empty) { return }
# env var override — CI / pre-exported token
let env_key = ($"CF_TOKEN_($secret_ref | str upcase | str replace --all '-' '_')")
let from_env = ($env | get -o $env_key | default "")
if ($from_env | is-not-empty) {
load-env { CF_API_TOKEN: $from_env }
return
}
# ensure SOPS_AGE_KEY_FILE is set — look for .kage in the infra tree
if ($env | get -o SOPS_AGE_KEY_FILE | default "" | is-empty) {
let kage_hits = (glob ($"($ws_path)/infra/*/.kage") | append (glob ($"($ws_path)/.p/.kage")))
if ($kage_hits | is-not-empty) {
load-env { SOPS_AGE_KEY_FILE: ($kage_hits | first) }
}
}
# auto-decrypt SOPS file: infra/*/secrets/<secret_ref>.sops.yaml
let matches = (glob ($"($ws_path)/infra/*/secrets/($secret_ref).sops.yaml"))
let sops_file = if ($matches | is-empty) { "" } else { $matches | first }
if ($sops_file | is-empty) {
error make --unspanned { msg: $"No SOPS file found for ($secret_ref) in ($ws_path)/infra/*/secrets/" }
}
let r = (do { ^sops --decrypt $sops_file } | complete)
if $r.exit_code != 0 {
error make --unspanned { msg: $"sops --decrypt failed for ($sops_file): ($r.stderr)" }
}
let token = ($r.stdout | from yaml | get -o token | default "")
if ($token | is-empty) {
error make --unspanned { msg: $"Key 'token' not found in ($sops_file)" }
}
load-env { CF_API_TOKEN: $token }
}
# List DNS records for a zone. Filter by --type or --source. Omit zone for interactive selector.
# With --all: always shows the full zone selector regardless of how many zones are configured.
export def "dns show" [
zone?: string
--all (-a)
--type (-t): string
--source: string
--raw (-r)
--workspace (-w): string
]: nothing -> table {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let zname = if $all {
let sel = ($zones | columns | input list "Select zone:")
if ($sel | is-empty) { error make --unspanned { msg: "No zone selected" } }
$sel
} else {
select_zone $zones $zone
}
set_cf_token $zones $zname $ws
let provider = (resolve_provider $zones $zname)
mut records = (mw_dns_record_list $provider $zname)
if ($type | is-not-empty) {
$records = ($records | where type == ($type | str upcase))
}
if ($source | is-not-empty) {
$records = ($records | where source == $source)
}
if $raw { return $records }
$records | sort-by type name | each {|r|
let val = if $r.type == "MX" and ($r.priority? | is-not-empty) {
$"($r.priority) ($r.value)"
} else if $r.type == "TXT" and ($r.value | str length) > 80 {
$"($r.value | str substring 0..79)…"
} else {
$r.value
}
{
type: $r.type,
name: $r.name,
value: $val,
ttl: $r.ttl,
px: (if ($r.proxied? | default false) { "✓" } else { "-" }),
}
}
}
# Add a DNS record. Omit args for interactive prompts.
# Example: dns add mail.example.com A 1.2.3.4 --zone example.com
export def "dns add" [
name?: string
type?: string
value?: string
--ttl (-l): int = 300
--zone (-z): string
--priority: int
--workspace (-w): string
--proxied (-p)
]: nothing -> record {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let resolved_zone = (select_zone $zones $zone)
set_cf_token $zones $resolved_zone $ws
let provider = (resolve_provider $zones $resolved_zone)
let rec_name = if ($name | is-not-empty) { $name } else {
let v = (input $" name (e.g. mail.($resolved_zone)): ")
if ($v | is-empty) { error make --unspanned { msg: "name is required" } }
$v
}
let rec_type = if ($type | is-not-empty) { $type | str upcase } else {
let sel = (["A" "AAAA" "CNAME" "MX" "TXT" "CAA" "NS" "SRV"] | input list "Type:")
if ($sel | is-empty) { error make --unspanned { msg: "type is required" } }
$sel
}
let rec_value = if ($value | is-not-empty) { $value } else {
let v = (input " value: ")
if ($v | is-empty) { error make --unspanned { msg: "value is required" } }
$v
}
let rec_ttl = if ($name | is-not-empty) { $ttl } else {
let v = (input $" ttl [($ttl)]: ")
if ($v | is-empty) { $ttl } else { $v | into int }
}
let rec_proxied = if $proxied { true } else if ($name | is-not-empty) { false } else {
(input " proxied y/n [n]: ") == "y"
}
let rec_priority = if ($priority | is-not-empty) { $priority } else if $rec_type == "MX" {
let v = (input " priority [10]: ")
if ($v | is-empty) { 10 } else { $v | into int }
} else { null }
mut rec = {
zone: $resolved_zone,
name: $rec_name,
type: $rec_type,
value: $rec_value,
ttl: $rec_ttl,
proxied: $rec_proxied,
source: "manual",
}
if ($rec_priority | is-not-empty) {
$rec = ($rec | upsert priority $rec_priority)
}
mw_dns_record_create $provider $rec
}
# Delete a DNS record by name and type. Requires --confirm.
export def "dns delete" [
name: string
type: string
--zone (-z): string
--workspace (-w): string
--confirm
]: nothing -> nothing {
if not $confirm {
error make --unspanned { msg: "Pass --confirm to delete a DNS record" }
}
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let resolved_zone = (select_zone $zones $zone)
set_cf_token $zones $resolved_zone $ws
let provider = (resolve_provider $zones $resolved_zone)
let rtype = ($type | str upcase)
let live = (mw_dns_record_list $provider $resolved_zone
| where { |r| $r.name == $name and $r.type == $rtype })
if ($live | is-empty) {
print $"No record found: ($name) ($rtype) in ($resolved_zone)"
return
}
mw_dns_record_delete $provider ($live | first).id $resolved_zone
print $"Deleted ($name) ($rtype) from ($resolved_zone)"
}
# Edit a DNS record interactively — select from live records, change value/ttl/proxied.
export def "dns edit" [
zone?: string
--workspace (-w): string
]: nothing -> nothing {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let zname = (select_zone $zones $zone)
set_cf_token $zones $zname $ws
let provider = (resolve_provider $zones $zname)
let records = (mw_dns_record_list $provider $zname | sort-by type name)
if ($records | is-empty) {
print $"No records found for ($zname)"
return
}
let labels = ($records | each {|r|
let px = if ($r.proxied? | default false) { "⚡" } else { " " }
let pri = if $r.type == "MX" and ($r.priority? | is-not-empty) { $"($r.priority) " } else { "" }
let val = if ($r.value | str length) > 55 { $"($r.value | str substring 0..54)…" } else { $r.value }
$"($r.type | fill -w 6)($px) ($r.name) → ($pri)($val)"
})
let idx = ($labels | input list --index "Select record:")
if ($idx | is-empty) { return }
let rec = ($records | get $idx)
print $" (ansi cyan)($rec.type)(ansi reset) ($rec.name)"
let action = (["Edit" "Delete"] | input list "Action:")
if ($action | is-empty) { return }
if $action == "Delete" {
let confirm = (input " Delete this record? y/N: ")
if $confirm != "y" { print "Cancelled."; return }
mw_dns_record_delete $provider $rec.id $zname
print $" deleted: ($rec.type) ($rec.name)"
return
}
let raw_value = (input $" value [($rec.value)]: ")
let new_value = if ($raw_value | is-empty) { $rec.value } else { $raw_value }
let raw_ttl = (input $" ttl [($rec.ttl)]: ")
let new_ttl = if ($raw_ttl | is-empty) { $rec.ttl } else { $raw_ttl | into int }
let cur_px = if ($rec.proxied? | default false) { "y" } else { "n" }
let raw_px = (input $" proxied [($cur_px)] y/n: ")
let new_proxied = if ($raw_px | is-empty) { $rec.proxied? | default false } else { $raw_px == "y" }
if $new_value == $rec.value and $new_ttl == $rec.ttl and $new_proxied == ($rec.proxied? | default false) {
print "No changes."
return
}
let updated = {
zone: $zname,
name: $rec.name,
type: $rec.type,
value: $new_value,
ttl: $new_ttl,
proxied: $new_proxied,
}
mw_dns_record_update $provider $rec.id $updated | ignore
print $" updated: ($rec.type) ($rec.name)"
}
# Reconcile static_records in dns_zones.ncl against live provider state.
# Skips records owned by external-dns (txt ownership) and docker-mailserver (label).
# With --apply: creates/updates; orphans always require manual confirmation.
export def "dns sync" [
--zone (-z): string
--workspace (-w): string
--dry-run
--apply
]: nothing -> table {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let target_zones = if ($zone | is-not-empty) { [$zone] } else { $zones | columns }
$target_zones | each {|zname|
let z = ($zones | get $zname)
set_cf_token $zones $zname $ws
let provider = (resolve_provider $zones $zname)
let declared = ($z.static_records? | default [])
let live = (mw_dns_record_list $provider $zname)
let live_manual = ($live | where { |r|
($r.source? | default "manual") == "manual"
})
let declared_keys = ($declared | each {|d|
{ zone: $zname, name: $d.name, type: ($d.type | into string) }
})
let missing = ($declared | where {|d|
let key = { zone: $zname, name: $d.name, type: ($d.type | into string) }
not ($live_manual | any { |r| $r.name == $d.name and $r.type == ($d.type | into string) })
} | each {|d| { action: "create", zone: $zname, name: $d.name, type: ($d.type | into string), live_value: "", declared_value: $d.value }})
let updates = ($declared | where {|d|
$live_manual | any { |r| $r.name == $d.name and $r.type == ($d.type | into string) and $r.value != $d.value }
} | each {|d|
let live_r = ($live_manual | where { |r| $r.name == $d.name and $r.type == ($d.type | into string) } | first)
{ action: "update", zone: $zname, name: $d.name, type: ($d.type | into string), live_value: $live_r.value, declared_value: $d.value }
})
let orphans = ($live_manual | where {|r|
not ($declared | any {|d| $d.name == $r.name and ($d.type | into string) == $r.type })
} | each {|r|
{ action: "orphan", zone: $zname, name: $r.name, type: $r.type, live_value: $r.value, declared_value: "" }
})
let diff_table = ($missing ++ $updates ++ $orphans)
if $apply and not $dry_run {
for row in ($missing ++ $updates) {
let rec = ($declared | where { |d| $d.name == $row.name and ($d.type | into string) == $row.type } | first)
mw_dns_record_upsert $provider ({ zone: $zname } | merge $rec) | ignore
print $" applied ($row.action): ($row.name) ($row.type)"
}
}
$diff_table
} | flatten
}
# Show zones with active infra component usage. Use --all to include unused zones.
export def "dns list" [
--all (-a)
--workspace (-w): string
]: nothing -> table {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let zone_names = ($zones | columns)
let comp_files = (glob ($"($ws)/infra/*/components/*.ncl"))
let usage = ($comp_files | each {|f|
let component = ($f | path basename | str replace --regex '\.ncl$' '')
let lines = (open --raw $f | lines)
$zone_names | each {|zone|
let fqdns = ($lines
| where { $in | str contains $zone }
| each {|line|
$line | parse --regex '"([^"]+)"'
| get capture0
}
| flatten
| each { $in | str replace --regex '^https?://' '' }
| uniq
| where { ($in | str ends-with $zone) and not ($in | str contains "@") }
)
$fqdns | each {|fqdn| { zone: $zone, component: $component, fqdn: $fqdn }}
} | flatten
} | flatten | uniq | sort-by zone component)
if not $all { return ($usage | sort-by zone component) }
let used_zones = ($usage | get zone | uniq)
let unused = ($zone_names
| where {|z| not ($used_zones | any {|u| $u == $z }) }
| each {|z| { zone: $z, component: "-", fqdn: "-" }}
)
$usage ++ $unused | sort-by zone component
}
# Query the live DKIM TXT record for a domain. Omit domain for interactive selector.
export def "dns dkim show" [
domain?: string
--selector (-s): string = "mail"
--workspace (-w): string
]: nothing -> record {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let zones = (load_zones $ws)
let zname = (select_zone $zones $domain)
let provider = (resolve_provider $zones $zname)
set_cf_token $zones $zname $ws
let dkim_name = $"($selector)._domainkey.($zname)"
let records = (mw_dns_record_list $provider $zname
| where { |r| $r.name == $dkim_name and $r.type == "TXT" })
if ($records | is-empty) {
error make --unspanned { msg: $"No DKIM TXT record found for ($dkim_name)" }
}
$records | first
}
def "main list" [
--all (-a)
--workspace (-w): string
]: nothing -> table {
dns list --all=$all --workspace $workspace
}
def "main l" [--workspace (-w): string]: nothing -> table {
dns list --workspace $workspace
}
def "main la" [--workspace (-w): string]: nothing -> table {
dns list --all --workspace $workspace
}
def "main s" [
zone?: string
--all (-a)
--type (-t): string
--source: string
--raw (-r)
--workspace (-w): string
]: nothing -> table {
dns show $zone --all=$all --type $type --source $source --raw=$raw --workspace $workspace
}
def "main sa" [
--type (-t): string
--source: string
--raw (-r)
--workspace (-w): string
]: nothing -> table {
dns show --all --type $type --source $source --raw=$raw --workspace $workspace
}
def "main e" [zone?: string --workspace (-w): string]: nothing -> nothing {
dns edit $zone --workspace $workspace
}
def "main a" [
name?: string
type?: string
value?: string
--ttl (-l): int = 300
--zone (-z): string
--priority: int
--workspace (-w): string
--proxied (-p)
]: nothing -> record {
dns add $name $type $value --ttl $ttl --zone $zone --priority $priority --workspace $workspace --proxied=$proxied
}
def "main d" [
name: string
type: string
--zone (-z): string
--workspace (-w): string
--confirm
]: nothing -> nothing {
dns delete $name $type --zone $zone --workspace $workspace --confirm=$confirm
}
def "main sy" [
--zone (-z): string
--workspace (-w): string
--dry-run
--apply
]: nothing -> table {
dns sync --zone $zone --workspace $workspace --dry-run=$dry_run --apply=$apply
}
def "main show" [
zone?: string
--all (-a)
--type (-t): string
--source: string
--raw (-r)
--workspace (-w): string
]: nothing -> table {
dns show $zone --all=$all --type $type --source $source --raw=$raw --workspace $workspace
}
def "main add" [
name?: string
type?: string
value?: string
--ttl (-l): int = 300
--zone (-z): string
--priority: int
--workspace (-w): string
--proxied (-p)
]: nothing -> record {
dns add $name $type $value --ttl $ttl --zone $zone --priority $priority --workspace $workspace --proxied=$proxied
}
def "main edit" [
zone?: string
--workspace (-w): string
]: nothing -> nothing {
dns edit $zone --workspace $workspace
}
def "main delete" [
name: string
type: string
--zone (-z): string
--workspace (-w): string
--confirm
]: nothing -> nothing {
dns delete $name $type --zone $zone --workspace $workspace --confirm=$confirm
}
def "main sync" [
--zone (-z): string
--workspace (-w): string
--dry-run
--apply
]: nothing -> table {
dns sync --zone $zone --workspace $workspace --dry-run=$dry_run --apply=$apply
}
def "main dkim show" [
domain?: string
--selector (-s): string = "mail"
--workspace (-w): string
]: nothing -> record {
dns dkim show $domain --selector $selector --workspace $workspace
}
# Collect dns_internal entries from all component NCL files in the workspace.
# Returns list of { host, zone, target, ttl, component } records.
def collect-dns-internal [ws: string, prov_root: string]: nothing -> list {
let comp_glob = ($ws | path join "infra" "*" "components" "*.ncl")
let split_file = ($ws | path join "infra" "libre-wuji" "components" "coredns_vpn_split.ncl")
(glob $comp_glob)
| where { $in != $split_file }
| each {|f|
let r = (do { ^nickel export --format json --import-path $prov_root $f } | complete)
if $r.exit_code != 0 { return [] }
let data = ($r.stdout | from json)
let comp = ($f | path basename | str replace --regex '\.ncl$' '')
$data | columns | each {|key|
let val = ($data | get $key)
let entries = if ($val | describe | str starts-with "record") {
$val.dns_internal? | default []
} else { [] }
$entries | each {|e| { host: $e.host, zone: $e.zone, target: $e.target, ttl: ($e.ttl? | default 300 | into int), component: $comp } }
} | flatten
} | flatten
}
# Generate the Nickel content for coredns_vpn_split.ncl from grouped zone data.
# zones_map: record where keys are zone names, values are lists of { host, target, ttl }.
def gen-split-ncl [zones_map: record]: nothing -> string {
let entries = ($zones_map | transpose zone records | each {|row|
let recs = ($row.records | sort-by host | each {|r|
$" \{ name = \"($r.host)\", value = \"($r.target)\", ttl = ($r.ttl), rectype = \"A\", etcd_dns_ttl = 300, etcd_peer_port = 2380, etcd_cli_port = 2379 },"
} | str join "\n")
$" \{
kind = 'private_zone,
domain = \"($row.zone)\",
port = 53,
bind = bind_ip,
records = [
($recs)
],
forward = \{ source = \".\", forward_ip = null },
use_log = true, use_errors = true, use_cache = false,
etcd_cluster_name = \"\",
},"
} | str join "\n")
$"# Generated by `prvng dns build-private` — do not edit manually.
# Source of truth: dns_internal fields in infra/*/components/*.ncl
# Re-run `prvng dns build-private --apply` to regenerate from current declarations.
fun bind_ip =>
\{
entries = [
($entries)
],
}"
}
# List all private DNS entries declared via dns_internal across all workspace components.
# Reads every component NCL file and aggregates dns_internal entries into a table.
export def "dns priv-list" [
--workspace (-w): string
--zone (-z): string
--raw (-r)
]: nothing -> table {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
let entries = (collect-dns-internal $ws $prov_root)
if ($entries | is-empty) {
print "No dns_internal entries found."
return []
}
let filtered = if ($zone | is-not-empty) {
$entries | where zone == $zone
} else { $entries }
if $raw { return $filtered }
$filtered
| sort-by zone host
| each {|r|
{
zone: $r.zone
host: $r.host
target: $r.target
ttl: $r.ttl
component: $r.component
}
}
}
# Reconcile dns_internal declarations across all components with coredns_vpn_split.ncl.
# Reads every component NCL file, collects dns_internal entries, groups by zone,
# and generates the canonical split file. With --apply: writes the file to disk.
# After --apply: run `just install-component coredns_vpn libre-wuji-cp-0` to deploy.
export def "dns build-private" [
--workspace (-w): string
--dry-run
--apply
]: nothing -> table {
let ws = ($workspace | default ($env.PROVISIONING_WORKSPACE? | default "."))
let prov_root = ($env.PROVISIONING? | default "/usr/local/provisioning")
let split_out = ($ws | path join "infra" "libre-wuji" "components" "coredns_vpn_split.ncl" | path expand)
let all_entries = (collect-dns-internal $ws $prov_root)
if ($all_entries | is-empty) {
print "No dns_internal entries found in any component."
return []
}
let zones_map = ($all_entries | group-by zone | transpose zone records
| each {|row|
let recs = ($row.records | each {|e| { host: $e.host, target: $e.target, ttl: $e.ttl } })
{ $row.zone: $recs }
} | reduce --fold {} {|it, acc| $acc | merge $it })
let new_content = (gen-split-ncl $zones_map)
# Read existing split file by exporting the function applied to "10.0.8.20".
# We write a wrapper NCL expression to a temp file and nickel-export it.
let existing_entries = if ($split_out | path exists) {
let tmpfile = ($nu.temp-dir | path join "dns_build_private_eval.ncl")
let import_expr = $"(char lparen)import \"($split_out)\"(char rparen) \"10.0.8.20\""
$import_expr | save --force $tmpfile
let r = (do { ^nickel export --format json $tmpfile } | complete)
if ($tmpfile | path exists) { rm $tmpfile }
if $r.exit_code == 0 {
($r.stdout | from json).entries? | default []
| each {|e|
let zone = ($e.domain? | default "")
$e.records? | default [] | each {|rec|
{ host: ($rec.name? | default ""), zone: $zone, target: ($rec.value? | default "") }
}
} | flatten
} else { [] }
} else { [] }
let desired_flat = ($all_entries | each {|e| { host: $e.host, zone: $e.zone, target: $e.target }})
let to_add = ($desired_flat | where {|d|
not ($existing_entries | any {|ex| $ex.host == $d.host and $ex.zone == $d.zone and $ex.target == $d.target })
} | each {|r| { action: "add", zone: $r.zone, host: $r.host, target: $r.target }})
let to_remove = ($existing_entries | where {|ex|
not ($desired_flat | any {|d| $d.host == $ex.host and $d.zone == $ex.zone })
} | each {|r| { action: "remove", zone: $r.zone, host: $r.host, target: $r.target }})
let diff = ($to_add ++ $to_remove | sort-by zone host)
if ($diff | is-empty) {
print $"(ansi green)✓(ansi reset) coredns_vpn_split.ncl is up to date — ($desired_flat | length) records across ($zones_map | columns | length) zones."
return $diff
}
if not $dry_run and $apply {
$new_content | save --force $split_out
print $"(ansi green)✓(ansi reset) Written ($split_out)"
print $" Run: just install-component coredns_vpn libre-wuji-cp-0"
} else {
let n_changes = ($diff | length)
print $"Diff ($n_changes) changes - pass --apply to write ($split_out | path basename):"
}
$diff
}
def print_help []: nothing -> nothing {
let title = "DNS Record Management"
let bar = (1..($title | str length) | each {|_| "=" } | str join "")
print $title
print $bar
print ""
print " Usage: prvng dns <subcommand> [options]"
print ""
print $" (ansi green)Read:(ansi reset)"
print $" (ansi cyan)list \(l\)(ansi reset) [--all/-a] zones with active component usage"
print $" (ansi cyan)la(ansi reset) alias: list --all"
print $" (ansi cyan)show \(s\)(ansi reset) [zone] [--all] [--type T] live records; --all forces zone selector"
print $" (ansi cyan)dkim show(ansi reset) [domain] [--selector S] live DKIM TXT record"
print ""
print $" (ansi green)Write:(ansi reset)"
print $" (ansi cyan)add \(a\)(ansi reset) [name type value] create record; prompts if args omitted"
print $" (ansi cyan)edit \(e\)(ansi reset) [zone] interactive editor and delete"
print $" (ansi cyan)delete \(d\)(ansi reset) <name> <type> --confirm remove record [--zone Z]"
print $" (ansi cyan)sync \(sy\)(ansi reset) [--zone Z] [--apply] reconcile static_records vs live"
print ""
print $" (ansi green)Flags for show:(ansi reset)"
print " --type / -t T filter by type (A, AAAA, CNAME, MX, TXT...)"
print " --source S filter by source"
print " --raw / -r return raw table, skip formatting"
print " --workspace / -w P override workspace path"
print ""
print $" (ansi green)Private DNS:(ansi reset)"
print $" (ansi cyan)priv-list (char lparen)pl(char rparen)(ansi reset) [--zone Z] [--raw] list all dns_internal records from components"
print $" (ansi cyan)build-private (char lparen)bp(char rparen)(ansi reset) [--apply] [--dry-run] sync coredns_vpn_split.ncl from dns_internal declarations"
print ""
print $" (ansi green)Aliases:(ansi reset)"
print " l -> list la -> list --all s -> show sa -> show --all"
print " e -> edit a -> add d -> delete sy -> sync"
print " pl -> priv-list bp -> build-private"
}
def "main help" []: nothing -> nothing { print_help }
def "main build-private" [--workspace (-w): string --dry-run --apply]: nothing -> table {
dns build-private --workspace $workspace --dry-run=$dry_run --apply=$apply
}
def "main bp" [--workspace (-w): string --dry-run --apply]: nothing -> table {
dns build-private --workspace $workspace --dry-run=$dry_run --apply=$apply
}
def "main priv-list" [--workspace (-w): string --zone (-z): string --raw (-r)]: nothing -> table {
dns priv-list --workspace $workspace --zone $zone --raw=$raw
}
def "main pl" [--workspace (-w): string --zone (-z): string --raw (-r)]: nothing -> table {
dns priv-list --workspace $workspace --zone $zone --raw=$raw
}
def main [...args: string]: nothing -> nothing {
let ws = ($env.PROVISIONING_WORKSPACE? | default ".")
let first = ($args | get -o 0 | default "")
let dns_cap = (load_capabilities $ws)
let aliases = ($dns_cap.provider_aliases? | default {})
if ($first | is-not-empty) and ($first in ($aliases | columns)) {
let provider = ($aliases | get $first | into string)
let rest = ($args | skip 1)
let sub = ($rest | get -o 0 | default "")
let sub_rest = ($rest | skip 1)
with-env { DNS_PROVIDER_OVERRIDE: $provider } {
match $sub {
"show" | "s" => {
let z = ($sub_rest | get -o 0)
if ($z | is-not-empty) {
dns show $z --workspace $ws
} else {
dns show --workspace $ws
}
}
"add" | "a" => {
dns add ($sub_rest | get -o 0) ($sub_rest | get -o 1) ($sub_rest | get -o 2) --workspace $ws
}
"edit" | "e" => {
let z = ($sub_rest | get -o 0)
if ($z | is-not-empty) {
dns edit $z --workspace $ws
} else {
dns edit --workspace $ws
}
}
"delete" | "d" => {
if ($sub_rest | length) < 2 {
error make --unspanned { msg: $"delete requires <name> <type> --confirm" }
}
dns delete ($sub_rest | get 0) ($sub_rest | get 1) --workspace $ws --confirm
}
"list" | "l" => { dns list --workspace $ws }
"sync" | "sy" => { dns sync --workspace $ws }
"dkim show" => {
let z = ($sub_rest | get -o 0)
if ($z | is-not-empty) {
dns dkim show $z --workspace $ws
} else {
dns dkim show --workspace $ws
}
}
_ => {
print $"Unknown subcommand: ($sub)"
print ""
print_help
}
}
}
} else {
match $first {
"" | "help" | "h" | "-h" | "--help" => { print_help }
_ => {
print $"Unknown subcommand: ($first)"
print ""
print_help
}
}
}
}