136 lines
3.7 KiB
Go
136 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ZoneFile is the in-memory representation of a dynamic OctoDNS zone YAML file.
|
|
// Keys are subdomain labels (empty string = apex record).
|
|
// Values are either a single record map or a list of record maps (for multi-type).
|
|
type ZoneFile map[string]interface{}
|
|
|
|
// RecordStats counts the changes between two zone states.
|
|
type RecordStats struct {
|
|
Added int
|
|
Removed int
|
|
Changed int
|
|
}
|
|
|
|
// ReadZoneFile parses an OctoDNS YAML zone file.
|
|
// If the file does not exist, returns an empty ZoneFile (not an error).
|
|
func ReadZoneFile(path string) (ZoneFile, error) {
|
|
data, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return ZoneFile{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read zone file %q: %w", path, err)
|
|
}
|
|
var zf ZoneFile
|
|
if err := yaml.Unmarshal(data, &zf); err != nil {
|
|
return nil, fmt.Errorf("parse zone file %q: %w", path, err)
|
|
}
|
|
if zf == nil {
|
|
zf = ZoneFile{}
|
|
}
|
|
return zf, nil
|
|
}
|
|
|
|
// WriteZoneFile writes an OctoDNS YAML zone file, creating parent directories as needed.
|
|
// yaml.v3 sorts map keys alphabetically, ensuring deterministic output.
|
|
func WriteZoneFile(path string, zf ZoneFile) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return fmt.Errorf("create directory for zone file: %w", err)
|
|
}
|
|
data, err := yaml.Marshal(map[string]interface{}(zf))
|
|
if err != nil {
|
|
return fmt.Errorf("marshal zone file: %w", err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
return fmt.Errorf("write zone file %q: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildZoneFile constructs the desired zone file from the set of active subdomains.
|
|
func BuildZoneFile(subdomains map[string]struct{}, cfg *Config) ZoneFile {
|
|
zf := make(ZoneFile, len(subdomains))
|
|
for sub := range subdomains {
|
|
zf[sub] = buildRecord(cfg)
|
|
}
|
|
return zf
|
|
}
|
|
|
|
// buildRecord creates the YAML-serialisable record value for a subdomain.
|
|
// Returns a single-record map (OctoDNS simple syntax) when only IPv4 is configured,
|
|
// or a two-element list (OctoDNS list syntax) when both A and AAAA are required.
|
|
func buildRecord(cfg *Config) interface{} {
|
|
aRec := map[string]interface{}{
|
|
"ttl": cfg.RecordTTL,
|
|
"type": "A",
|
|
"values": []string{cfg.PublicIP},
|
|
}
|
|
if cfg.CloudflareAutoTTL {
|
|
aRec["octodns"] = map[string]interface{}{
|
|
"cloudflare": map[string]interface{}{
|
|
"auto-ttl": true,
|
|
},
|
|
}
|
|
}
|
|
if cfg.PublicIPv6 == "" {
|
|
return aRec
|
|
}
|
|
aaaaRec := map[string]interface{}{
|
|
"ttl": cfg.RecordTTL,
|
|
"type": "AAAA",
|
|
"values": []string{cfg.PublicIPv6},
|
|
}
|
|
if cfg.CloudflareAutoTTL {
|
|
aaaaRec["octodns"] = map[string]interface{}{
|
|
"cloudflare": map[string]interface{}{
|
|
"auto-ttl": true,
|
|
},
|
|
}
|
|
}
|
|
return []interface{}{aRec, aaaaRec}
|
|
}
|
|
|
|
// DiffZoneFile computes the difference between the current (on-disk) and desired zone files.
|
|
// It reports how many records were added, removed, or changed.
|
|
func DiffZoneFile(current, desired ZoneFile) RecordStats {
|
|
var stats RecordStats
|
|
for k := range desired {
|
|
if _, exists := current[k]; !exists {
|
|
stats.Added++
|
|
}
|
|
}
|
|
for k := range current {
|
|
if _, exists := desired[k]; !exists {
|
|
stats.Removed++
|
|
}
|
|
}
|
|
// Detect value changes (e.g. IP or TTL updated in config).
|
|
for k, dv := range desired {
|
|
cv, exists := current[k]
|
|
if !exists {
|
|
continue
|
|
}
|
|
// Marshal both sides to compare: handles nested maps and slices uniformly.
|
|
cs, _ := yaml.Marshal(cv)
|
|
ds, _ := yaml.Marshal(dv)
|
|
if string(cs) != string(ds) {
|
|
stats.Changed++
|
|
}
|
|
}
|
|
return stats
|
|
}
|
|
|
|
// ZoneFilePath returns the absolute path to the dynamic zone YAML file for a given zone.
|
|
func ZoneFilePath(cfg *Config, zone string) string {
|
|
return filepath.Join(cfg.RepoPath, cfg.DynamicDir, zone+".yaml")
|
|
}
|