359 lines
9.9 KiB
Text
359 lines
9.9 KiB
Text
|
|
# CoreDNS Corefile Generator
|
||
|
|
# Generates and manages Corefile configuration for CoreDNS
|
||
|
|
|
||
|
|
# ../utils/log.nu does not exist — dangling import removed (ADR-025 L2).
|
||
|
|
|
||
|
|
# Generate Corefile from configuration
|
||
|
|
export def generate-corefile [
|
||
|
|
config: record # CoreDNS configuration
|
||
|
|
] -> string {
|
||
|
|
log debug $"Generating Corefile from config: ($config)"
|
||
|
|
|
||
|
|
let local_config = $config.local? | default {}
|
||
|
|
let zones = $local_config.zones? | default ["provisioning.local"]
|
||
|
|
let port = $local_config.port? | default 5353
|
||
|
|
let zones_path = $local_config.zones_path? | default "~/.provisioning/coredns/zones"
|
||
|
|
let upstream = $config.upstream? | default ["8.8.8.8", "1.1.1.1"]
|
||
|
|
let enable_logging = $config.enable_logging? | default true
|
||
|
|
let enable_metrics = $config.enable_metrics? | default true
|
||
|
|
let metrics_port = $config.metrics_port? | default 9153
|
||
|
|
|
||
|
|
let dynamic_enabled = $config.dynamic_updates?.enabled? | default true
|
||
|
|
let dynamic_endpoint = $config.dynamic_updates?.api_endpoint? | default "http://localhost:8080/dns"
|
||
|
|
|
||
|
|
mut corefile_content = []
|
||
|
|
|
||
|
|
# Generate zone blocks
|
||
|
|
for zone in $zones {
|
||
|
|
let zone_block = generate-zone-block $zone $port $zones_path $enable_logging $dynamic_enabled
|
||
|
|
$corefile_content = ($corefile_content | append $zone_block)
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate catch-all forward block
|
||
|
|
let forward_block = generate-forward-block $port $upstream $enable_logging
|
||
|
|
$corefile_content = ($corefile_content | append $forward_block)
|
||
|
|
|
||
|
|
# Generate metrics block if enabled
|
||
|
|
if $enable_metrics {
|
||
|
|
let metrics_block = generate-metrics-block $metrics_port
|
||
|
|
$corefile_content = ($corefile_content | append $metrics_block)
|
||
|
|
}
|
||
|
|
|
||
|
|
$corefile_content | str join "\n\n"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate zone block for Corefile
|
||
|
|
def generate-zone-block [
|
||
|
|
zone: string
|
||
|
|
port: int
|
||
|
|
zones_path: string
|
||
|
|
enable_logging: bool
|
||
|
|
dynamic_enabled: bool
|
||
|
|
] -> string {
|
||
|
|
let expanded_path = $zones_path | path expand
|
||
|
|
|
||
|
|
mut plugins = []
|
||
|
|
|
||
|
|
# File plugin
|
||
|
|
$plugins = ($plugins | append $" file ($expanded_path)/($zone).zone")
|
||
|
|
|
||
|
|
# Dynamic updates via HTTP (if enabled)
|
||
|
|
if $dynamic_enabled {
|
||
|
|
$plugins = ($plugins | append " # Dynamic DNS updates via orchestrator API")
|
||
|
|
$plugins = ($plugins | append " # Updates handled through external API")
|
||
|
|
}
|
||
|
|
|
||
|
|
# Logging
|
||
|
|
if $enable_logging {
|
||
|
|
$plugins = ($plugins | append " log")
|
||
|
|
}
|
||
|
|
|
||
|
|
# Error handling
|
||
|
|
$plugins = ($plugins | append " errors")
|
||
|
|
|
||
|
|
# Cache
|
||
|
|
$plugins = ($plugins | append " cache 30")
|
||
|
|
|
||
|
|
let plugins_str = $plugins | str join "\n"
|
||
|
|
|
||
|
|
$"($zone):($port) {
|
||
|
|
($plugins_str)
|
||
|
|
}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate forward block for Corefile
|
||
|
|
def generate-forward-block [
|
||
|
|
port: int
|
||
|
|
upstream: list<string>
|
||
|
|
enable_logging: bool
|
||
|
|
] -> string {
|
||
|
|
let upstream_str = $upstream | str join " "
|
||
|
|
|
||
|
|
mut plugins = []
|
||
|
|
$plugins = ($plugins | append $" forward . ($upstream_str)")
|
||
|
|
|
||
|
|
if $enable_logging {
|
||
|
|
$plugins = ($plugins | append " log")
|
||
|
|
}
|
||
|
|
|
||
|
|
$plugins = ($plugins | append " errors")
|
||
|
|
$plugins = ($plugins | append " cache 30")
|
||
|
|
|
||
|
|
let plugins_str = $plugins | str join "\n"
|
||
|
|
|
||
|
|
$".:($port) {
|
||
|
|
($plugins_str)
|
||
|
|
}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate metrics block for Corefile
|
||
|
|
def generate-metrics-block [
|
||
|
|
metrics_port: int
|
||
|
|
] -> string {
|
||
|
|
$".:($metrics_port) {
|
||
|
|
prometheus
|
||
|
|
errors
|
||
|
|
}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate zone file for domain
|
||
|
|
export def generate-zone-file [
|
||
|
|
zone_name: string # Zone name (e.g., "provisioning.local")
|
||
|
|
records: list # List of DNS records
|
||
|
|
config: record # CoreDNS configuration
|
||
|
|
] -> string {
|
||
|
|
log debug $"Generating zone file for ($zone_name)"
|
||
|
|
|
||
|
|
let default_ttl = $config.default_ttl? | default 3600
|
||
|
|
let admin_email = $"admin.($zone_name)"
|
||
|
|
let serial = date now | format date "%Y%m%d01" # YYYYMMDDnn format
|
||
|
|
|
||
|
|
mut zone_content = []
|
||
|
|
|
||
|
|
# Origin and TTL
|
||
|
|
$zone_content = ($zone_content | append $"$ORIGIN ($zone_name).")
|
||
|
|
$zone_content = ($zone_content | append $"$TTL ($default_ttl)")
|
||
|
|
$zone_content = ($zone_content | append "")
|
||
|
|
|
||
|
|
# SOA record
|
||
|
|
let soa_block = generate-soa-record $zone_name $admin_email $serial
|
||
|
|
$zone_content = ($zone_content | append $soa_block)
|
||
|
|
$zone_content = ($zone_content | append "")
|
||
|
|
|
||
|
|
# NS record
|
||
|
|
$zone_content = ($zone_content | append $"@ IN NS ns1.($zone_name).")
|
||
|
|
$zone_content = ($zone_content | append "")
|
||
|
|
|
||
|
|
# A record for nameserver
|
||
|
|
$zone_content = ($zone_content | append "ns1 IN A 127.0.0.1")
|
||
|
|
$zone_content = ($zone_content | append "")
|
||
|
|
|
||
|
|
# User-defined records
|
||
|
|
if ($records | is-not-empty) {
|
||
|
|
$zone_content = ($zone_content | append "; Infrastructure records")
|
||
|
|
|
||
|
|
for record in $records {
|
||
|
|
let record_line = generate-record-line $record
|
||
|
|
$zone_content = ($zone_content | append $record_line)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$zone_content | str join "\n"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate SOA record
|
||
|
|
def generate-soa-record [
|
||
|
|
zone_name: string
|
||
|
|
admin_email: string
|
||
|
|
serial: string
|
||
|
|
] -> string {
|
||
|
|
$"@ IN SOA ns1.($zone_name). ($admin_email). (
|
||
|
|
($serial) ; Serial
|
||
|
|
3600 ; Refresh
|
||
|
|
1800 ; Retry
|
||
|
|
604800 ; Expire
|
||
|
|
86400 ) ; Minimum TTL"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Generate DNS record line
|
||
|
|
def generate-record-line [
|
||
|
|
record: record
|
||
|
|
] -> string {
|
||
|
|
let name = $record.name
|
||
|
|
let type = $record.type
|
||
|
|
let value = $record.value
|
||
|
|
let ttl = $record.ttl? | default ""
|
||
|
|
let priority = $record.priority? | default ""
|
||
|
|
let comment = $record.comment? | default ""
|
||
|
|
|
||
|
|
mut parts = []
|
||
|
|
|
||
|
|
# Name (padded to 16 chars)
|
||
|
|
$parts = ($parts | append ($name | fill -a left -w 16))
|
||
|
|
|
||
|
|
# TTL (optional)
|
||
|
|
if ($ttl != "") {
|
||
|
|
$parts = ($parts | append ($ttl | into string | fill -a left -w 8))
|
||
|
|
} else {
|
||
|
|
$parts = ($parts | append ("" | fill -a left -w 8))
|
||
|
|
}
|
||
|
|
|
||
|
|
# Class (always IN)
|
||
|
|
$parts = ($parts | append "IN")
|
||
|
|
|
||
|
|
# Type
|
||
|
|
$parts = ($parts | append ($type | fill -a left -w 8))
|
||
|
|
|
||
|
|
# Priority (for MX and SRV)
|
||
|
|
if ($priority != "") {
|
||
|
|
$parts = ($parts | append ($priority | into string))
|
||
|
|
}
|
||
|
|
|
||
|
|
# Value
|
||
|
|
$parts = ($parts | append $value)
|
||
|
|
|
||
|
|
let record_line = $parts | str join " "
|
||
|
|
|
||
|
|
# Add comment if present
|
||
|
|
if ($comment != "") {
|
||
|
|
$"($record_line) ; ($comment)"
|
||
|
|
} else {
|
||
|
|
$record_line
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Validate Corefile syntax
|
||
|
|
export def validate-corefile [
|
||
|
|
corefile_path: string # Path to Corefile
|
||
|
|
] -> record {
|
||
|
|
log debug $"Validating Corefile at ($corefile_path)"
|
||
|
|
|
||
|
|
let expanded_path = $corefile_path | path expand
|
||
|
|
|
||
|
|
if not ($expanded_path | path exists) {
|
||
|
|
return {
|
||
|
|
valid: false
|
||
|
|
errors: ["Corefile not found"]
|
||
|
|
warnings: []
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
mut errors = []
|
||
|
|
mut warnings = []
|
||
|
|
|
||
|
|
let content = open $expanded_path
|
||
|
|
|
||
|
|
# Basic validation checks
|
||
|
|
if ($content | str contains "file ") {
|
||
|
|
# Check if zone files are referenced
|
||
|
|
log debug "Zone files referenced in Corefile"
|
||
|
|
} else {
|
||
|
|
$warnings = ($warnings | append "No zone files referenced")
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($content | str contains "forward ") {
|
||
|
|
log debug "Forward plugin configured"
|
||
|
|
} else {
|
||
|
|
$warnings = ($warnings | append "No forward plugin configured")
|
||
|
|
}
|
||
|
|
|
||
|
|
# Check for balanced braces
|
||
|
|
let open_braces = $content | str replace -a -r '[^{]' '' | str length
|
||
|
|
let close_braces = $content | str replace -a -r '[^}]' '' | str length
|
||
|
|
|
||
|
|
if $open_braces != $close_braces {
|
||
|
|
$errors = ($errors | append "Unbalanced braces in Corefile")
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
valid: ($errors | is-empty)
|
||
|
|
errors: $errors
|
||
|
|
warnings: $warnings
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add plugin to Corefile
|
||
|
|
export def add-corefile-plugin [
|
||
|
|
corefile_path: string # Path to Corefile
|
||
|
|
zone: string # Zone name
|
||
|
|
plugin_config: string # Plugin configuration line
|
||
|
|
] -> bool {
|
||
|
|
log debug $"Adding plugin to Corefile zone ($zone)"
|
||
|
|
|
||
|
|
let expanded_path = $corefile_path | path expand
|
||
|
|
|
||
|
|
if not ($expanded_path | path exists) {
|
||
|
|
log error $"Corefile not found at ($expanded_path)"
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
let content = open $expanded_path
|
||
|
|
|
||
|
|
# Find zone block and add plugin
|
||
|
|
let zone_pattern = $"($zone):\\d+ \\{"
|
||
|
|
|
||
|
|
if ($content | str contains -r $zone_pattern) {
|
||
|
|
# Insert plugin before closing brace
|
||
|
|
let updated = $content | str replace -r $"($zone_pattern)(.*?)\\}" $"$1$2 ($plugin_config)\n}"
|
||
|
|
$updated | save -f $expanded_path
|
||
|
|
log info $"Added plugin to ($zone) in Corefile"
|
||
|
|
true
|
||
|
|
} else {
|
||
|
|
log error $"Zone ($zone) not found in Corefile"
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Write Corefile to disk
|
||
|
|
export def write-corefile [
|
||
|
|
corefile_path: string # Path to write Corefile
|
||
|
|
content: string # Corefile content
|
||
|
|
] -> bool {
|
||
|
|
log debug $"Writing Corefile to ($corefile_path)"
|
||
|
|
|
||
|
|
let expanded_path = $corefile_path | path expand
|
||
|
|
let parent_dir = $expanded_path | path dirname
|
||
|
|
|
||
|
|
# Create parent directory if needed
|
||
|
|
if not ($parent_dir | path exists) {
|
||
|
|
mkdir $parent_dir
|
||
|
|
}
|
||
|
|
|
||
|
|
let result = (do {
|
||
|
|
$content | save -f $expanded_path
|
||
|
|
} | complete)
|
||
|
|
|
||
|
|
if $result.exit_code == 0 {
|
||
|
|
log info $"Corefile written to [$expanded_path]"
|
||
|
|
true
|
||
|
|
} else {
|
||
|
|
log error $"Failed to write Corefile to [$expanded_path]"
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Read Corefile from disk
|
||
|
|
export def read-corefile [
|
||
|
|
corefile_path: string # Path to Corefile
|
||
|
|
] -> string {
|
||
|
|
let expanded_path = $corefile_path | path expand
|
||
|
|
|
||
|
|
if not ($expanded_path | path exists) {
|
||
|
|
log error $"Corefile not found at ($expanded_path)"
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
open $expanded_path
|
||
|
|
}
|
||
|
|
|
||
|
|
# Update Corefile with new configuration
|
||
|
|
export def update-corefile [
|
||
|
|
config: record # CoreDNS configuration
|
||
|
|
corefile_path: string # Path to Corefile
|
||
|
|
] -> bool {
|
||
|
|
log info "Updating Corefile with new configuration"
|
||
|
|
|
||
|
|
let new_content = generate-corefile $config
|
||
|
|
write-corefile $corefile_path $new_content
|
||
|
|
}
|