Add catalog-zone-gen tool

Generate RFC 9432 DNS catalog zone files from a declarative input file.
Parses zone-to-catalog assignments with optional group and coo properties,
produces deterministic BIND-format output with automatic SOA serial
management and change detection.
This commit is contained in:
2026-02-28 16:13:58 -08:00
parent 5f230676d7
commit 1f2f39f40c
14 changed files with 1944 additions and 0 deletions

141
config_test.go Normal file
View File

@@ -0,0 +1,141 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestLoadConfig(t *testing.T) {
t.Run("valid config", func(t *testing.T) {
dir := t.TempDir()
writeTestFile(t, dir, "catz.yaml", `catalogs:
cat1:
zone: catalog1.example.com.
soa:
mname: ns1.example.com.
rname: hostmaster.example.com.
`)
cfg, err := loadConfig(filepath.Join(dir, "catz.yaml"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Catalogs["cat1"].Zone != "catalog1.example.com." {
t.Errorf("zone = %q, want %q", cfg.Catalogs["cat1"].Zone, "catalog1.example.com.")
}
})
t.Run("normalizes FQDNs", func(t *testing.T) {
dir := t.TempDir()
writeTestFile(t, dir, "catz.yaml", `catalogs:
cat1:
zone: Catalog1.Example.COM
soa:
mname: NS1.Example.COM
rname: Hostmaster.Example.COM
`)
cfg, err := loadConfig(filepath.Join(dir, "catz.yaml"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Catalogs["cat1"].Zone != "catalog1.example.com." {
t.Errorf("zone = %q, want normalized FQDN", cfg.Catalogs["cat1"].Zone)
}
if cfg.SOA.Mname != "ns1.example.com." {
t.Errorf("mname = %q, want normalized FQDN", cfg.SOA.Mname)
}
})
t.Run("missing file", func(t *testing.T) {
_, err := loadConfig("/nonexistent/catz.yaml")
if err == nil {
t.Fatal("expected error for missing file")
}
})
t.Run("invalid YAML", func(t *testing.T) {
dir := t.TempDir()
writeTestFile(t, dir, "catz.yaml", "{{invalid yaml")
_, err := loadConfig(filepath.Join(dir, "catz.yaml"))
if err == nil {
t.Fatal("expected error for invalid YAML")
}
})
}
func TestValidateConfig(t *testing.T) {
t.Run("no catalogs", func(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{},
SOA: SOAConfig{Mname: "ns1.example.com.", Rname: "hostmaster.example.com."},
}
if err := validateConfig(cfg); err == nil {
t.Fatal("expected error for no catalogs")
}
})
t.Run("nil catalogs", func(t *testing.T) {
cfg := &Config{
SOA: SOAConfig{Mname: "ns1.example.com.", Rname: "hostmaster.example.com."},
}
if err := validateConfig(cfg); err == nil {
t.Fatal("expected error for nil catalogs")
}
})
t.Run("empty zone", func(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{"cat1": {Zone: ""}},
SOA: SOAConfig{Mname: "ns1.example.com.", Rname: "hostmaster.example.com."},
}
if err := validateConfig(cfg); err == nil {
t.Fatal("expected error for empty zone")
}
})
t.Run("missing mname", func(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{"cat1": {Zone: "cat.example.com."}},
SOA: SOAConfig{Rname: "hostmaster.example.com."},
}
if err := validateConfig(cfg); err == nil {
t.Fatal("expected error for missing mname")
}
})
t.Run("missing rname", func(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{"cat1": {Zone: "cat.example.com."}},
SOA: SOAConfig{Mname: "ns1.example.com."},
}
if err := validateConfig(cfg); err == nil {
t.Fatal("expected error for missing rname")
}
})
t.Run("valid config", func(t *testing.T) {
cfg := &Config{
Catalogs: map[string]CatalogConfig{"cat1": {Zone: "cat.example.com."}},
SOA: SOAConfig{Mname: "ns1.example.com.", Rname: "hostmaster.example.com."},
}
if err := validateConfig(cfg); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
func TestLoadConfigWritePermission(t *testing.T) {
// Test that loadConfig returns error for unreadable file
dir := t.TempDir()
path := filepath.Join(dir, "catz.yaml")
writeTestFile(t, dir, "catz.yaml", "catalogs: {}")
if err := os.Chmod(path, 0o000); err != nil {
t.Skip("cannot change file permissions")
}
defer os.Chmod(path, 0o644)
_, err := loadConfig(path)
if err == nil {
t.Fatal("expected error for unreadable file")
}
}