Allow catalog-only zones without file= in --bind-conf mode
Zones without a file= property (e.g. ddns zones) are included in catalog zone output for secondaries but skipped in domains.conf. Previously --bind-conf required every zone to have file= set.
This commit is contained in:
@@ -76,7 +76,7 @@ Blank lines are ignored.
|
|||||||
| Bare names | identifier | Catalog assignments — must match a key in the config `catalogs` map. At least one required. |
|
| Bare names | identifier | Catalog assignments — must match a key in the config `catalogs` map. At least one required. |
|
||||||
| `group=<value>` | key=value | RFC 9432 group property. Tells consumers to apply shared configuration to grouped zones. |
|
| `group=<value>` | key=value | RFC 9432 group property. Tells consumers to apply shared configuration to grouped zones. |
|
||||||
| `coo=<fqdn>` | key=value | RFC 9432 change-of-ownership property. Points to the old catalog zone during migration. |
|
| `coo=<fqdn>` | key=value | RFC 9432 change-of-ownership property. Points to the old catalog zone during migration. |
|
||||||
| `file=<path>` | key=value | Zone data file path for BIND config output. Required when `--bind-conf` is used. |
|
| `file=<path>` | key=value | Zone data file path for BIND config output. Zones without `file=` are included in catalog zones but skipped in `--bind-conf` output. |
|
||||||
| `dnssec` | bare flag | Adds `dnssec-policy standard; inline-signing yes;` to the BIND config for this zone. |
|
| `dnssec` | bare flag | Adds `dnssec-policy standard; inline-signing yes;` to the BIND config for this zone. |
|
||||||
|
|
||||||
A zone can appear in multiple catalogs (for distributing to different server groups).
|
A zone can appear in multiple catalogs (for distributing to different server groups).
|
||||||
@@ -169,7 +169,7 @@ error: zones.txt:5: zone example.com. already assigned to catalog "catalog1" at
|
|||||||
- Unknown properties (anything other than `group`, `coo`, `file`) — error
|
- Unknown properties (anything other than `group`, `coo`, `file`) — error
|
||||||
- Empty `file=` value — error
|
- Empty `file=` value — error
|
||||||
- `dnssec=<anything>` (dnssec is a bare flag, not a key=value) — error
|
- `dnssec=<anything>` (dnssec is a bare flag, not a key=value) — error
|
||||||
- When `--bind-conf` is used: any zone missing `file=` — error
|
- When `--bind-conf` is used: zones without `file=` are silently skipped (included in catalog output only)
|
||||||
|
|
||||||
## BIND Config Output
|
## BIND Config Output
|
||||||
|
|
||||||
@@ -197,4 +197,4 @@ zone "bitcard.org" {
|
|||||||
- 8-space indentation
|
- 8-space indentation
|
||||||
- DNSSEC zones (marked with `dnssec` in the input) get `dnssec-policy standard;
|
- DNSSEC zones (marked with `dnssec` in the input) get `dnssec-policy standard;
|
||||||
inline-signing yes;` on the same line as `file`
|
inline-signing yes;` on the same line as `file`
|
||||||
- Every zone must have a `file=` property when `--bind-conf` is used
|
- Zones without a `file=` property are skipped (they appear in catalog zones only)
|
||||||
|
|||||||
21
bindconf.go
21
bindconf.go
@@ -23,6 +23,10 @@ func generateBindConf(entries []ZoneEntry) string {
|
|||||||
b.WriteString("#\n")
|
b.WriteString("#\n")
|
||||||
|
|
||||||
for _, entry := range sorted {
|
for _, entry := range sorted {
|
||||||
|
if entry.ZoneFile == "" {
|
||||||
|
continue // catalog-only zone, no BIND config needed
|
||||||
|
}
|
||||||
|
|
||||||
// Strip trailing dot for BIND zone name
|
// Strip trailing dot for BIND zone name
|
||||||
zoneName := strings.TrimSuffix(entry.Zone, ".")
|
zoneName := strings.TrimSuffix(entry.Zone, ".")
|
||||||
|
|
||||||
@@ -40,22 +44,9 @@ func generateBindConf(entries []ZoneEntry) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateBindConf checks that every zone entry has a non-empty ZoneFile.
|
// writeBindConf writes the BIND config to path.
|
||||||
func validateBindConf(entries []ZoneEntry) error {
|
// Zones without a ZoneFile are skipped (catalog-only zones).
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.ZoneFile == "" {
|
|
||||||
return fmt.Errorf("%s:%d: zone %s missing file= property (required for --bind-conf)",
|
|
||||||
entry.ZonesFile, entry.Line, entry.Zone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBindConf validates entries and writes the BIND config to path.
|
|
||||||
func writeBindConf(path string, entries []ZoneEntry) error {
|
func writeBindConf(path string, entries []ZoneEntry) error {
|
||||||
if err := validateBindConf(entries); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
content := generateBindConf(entries)
|
content := generateBindConf(entries)
|
||||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
return fmt.Errorf("writing bind config %s: %w", path, err)
|
return fmt.Errorf("writing bind config %s: %w", path, err)
|
||||||
|
|||||||
@@ -73,23 +73,30 @@ func TestValidateBindConf(t *testing.T) {
|
|||||||
{Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1},
|
{Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1},
|
||||||
{Zone: "b.example.com.", ZoneFile: "data/b", ZonesFile: "zones.txt", Line: 2},
|
{Zone: "b.example.com.", ZoneFile: "data/b", ZonesFile: "zones.txt", Line: 2},
|
||||||
}
|
}
|
||||||
if err := validateBindConf(entries); err != nil {
|
if err := writeBindConf(filepath.Join(t.TempDir(), "domains.conf"), entries); err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("missing file", func(t *testing.T) {
|
t.Run("zones without file are skipped", func(t *testing.T) {
|
||||||
entries := []ZoneEntry{
|
entries := []ZoneEntry{
|
||||||
{Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1},
|
{Zone: "a.example.com.", ZoneFile: "data/a", ZonesFile: "zones.txt", Line: 1},
|
||||||
{Zone: "b.example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 3},
|
{Zone: "dyn.example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 3},
|
||||||
}
|
}
|
||||||
err := validateBindConf(entries)
|
dir := t.TempDir()
|
||||||
if err == nil {
|
path := filepath.Join(dir, "domains.conf")
|
||||||
t.Fatal("expected error for missing file")
|
if err := writeBindConf(path, entries); err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := string(data)
|
||||||
|
assertContains(t, got, `zone "a.example.com"`)
|
||||||
|
if strings.Contains(got, "dyn.example.com") {
|
||||||
|
t.Error("catalog-only zone without file= should not appear in BIND config")
|
||||||
}
|
}
|
||||||
assertContains(t, err.Error(), "zones.txt:3:")
|
|
||||||
assertContains(t, err.Error(), "b.example.com.")
|
|
||||||
assertContains(t, err.Error(), "missing file=")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +124,7 @@ func TestWriteBindConf(t *testing.T) {
|
|||||||
assertContains(t, got, "dnssec-policy standard")
|
assertContains(t, got, "dnssec-policy standard")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWriteBindConfValidationError(t *testing.T) {
|
func TestWriteBindConfSkipsCatalogOnlyZones(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
path := filepath.Join(dir, "domains.conf")
|
path := filepath.Join(dir, "domains.conf")
|
||||||
|
|
||||||
@@ -125,14 +132,19 @@ func TestWriteBindConfValidationError(t *testing.T) {
|
|||||||
{Zone: "example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 5},
|
{Zone: "example.com.", ZoneFile: "", ZonesFile: "zones.txt", Line: 5},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := writeBindConf(path, entries)
|
if err := writeBindConf(path, entries); err != nil {
|
||||||
if err == nil {
|
t.Fatalf("unexpected error: %v", err)
|
||||||
t.Fatal("expected validation error")
|
|
||||||
}
|
}
|
||||||
assertContains(t, err.Error(), "zones.txt:5:")
|
|
||||||
|
|
||||||
// File should not have been written
|
data, err := os.ReadFile(path)
|
||||||
if _, statErr := os.Stat(path); statErr == nil {
|
if err != nil {
|
||||||
t.Error("file should not be written when validation fails")
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := string(data)
|
||||||
|
|
||||||
|
// Should have header but no zone blocks
|
||||||
|
assertContains(t, got, "# THIS FILE IS GENERATED BY catalog-zone-gen")
|
||||||
|
if strings.Contains(got, "example.com") {
|
||||||
|
t.Error("zone without file= should not appear in BIND config")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
main_test.go
28
main_test.go
@@ -482,7 +482,7 @@ zone.example.com catalog1, file=data/zones/example.com
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIntegrationCLIBindConfMissingFile(t *testing.T) {
|
func TestIntegrationCLIBindConfCatalogOnlyZone(t *testing.T) {
|
||||||
binary := filepath.Join(t.TempDir(), "catalog-zone-gen")
|
binary := filepath.Join(t.TempDir(), "catalog-zone-gen")
|
||||||
cmd := exec.Command("go", "build", "-o", binary, ".")
|
cmd := exec.Command("go", "build", "-o", binary, ".")
|
||||||
cmd.Dir = projectDir(t)
|
cmd.Dir = projectDir(t)
|
||||||
@@ -502,7 +502,7 @@ soa:
|
|||||||
`
|
`
|
||||||
writeTestFile(t, dir, "catz.yaml", configContent)
|
writeTestFile(t, dir, "catz.yaml", configContent)
|
||||||
|
|
||||||
// Zone without file= property
|
// Zone without file= property (catalog-only, e.g. ddns zone)
|
||||||
inputContent := "zone.example.org catalog1\n"
|
inputContent := "zone.example.org catalog1\n"
|
||||||
writeTestFile(t, dir, "zones.txt", inputContent)
|
writeTestFile(t, dir, "zones.txt", inputContent)
|
||||||
|
|
||||||
@@ -514,8 +514,26 @@ soa:
|
|||||||
"--bind-conf", bindConfPath,
|
"--bind-conf", bindConfPath,
|
||||||
filepath.Join(dir, "zones.txt"))
|
filepath.Join(dir, "zones.txt"))
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatal("expected error when file= is missing with --bind-conf")
|
t.Fatalf("unexpected error: %v\n%s", err, out)
|
||||||
}
|
}
|
||||||
assertContains(t, string(out), "missing file=")
|
|
||||||
|
// domains.conf should exist but not contain the catalog-only zone
|
||||||
|
data, readErr := os.ReadFile(bindConfPath)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatalf("failed to read domains.conf: %v", readErr)
|
||||||
|
}
|
||||||
|
got := string(data)
|
||||||
|
assertContains(t, got, "# THIS FILE IS GENERATED BY catalog-zone-gen")
|
||||||
|
if strings.Contains(got, "zone.example.org") {
|
||||||
|
t.Error("catalog-only zone without file= should not appear in domains.conf")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catalog zone should still be generated
|
||||||
|
catZonePath := filepath.Join(dir, "catalog1.example.com.zone")
|
||||||
|
catData, catErr := os.ReadFile(catZonePath)
|
||||||
|
if catErr != nil {
|
||||||
|
t.Fatalf("catalog zone not written: %v", catErr)
|
||||||
|
}
|
||||||
|
assertContains(t, string(catData), "zone.example.org")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user