Generate a BIND-format domains.conf file alongside catalog zones. New input properties: file= (zone data path) and dnssec (bare flag). When --bind-conf is set, every zone must have file= or it errors. Renames ZoneEntry.File to ZonesFile (input path for error messages) and adds ZoneFile (BIND file path) and DNSSEC (bool) fields.
143 lines
4.0 KiB
Go
143 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// ZoneEntry represents a parsed line from the input file.
|
|
type ZoneEntry struct {
|
|
Zone string // Normalized FQDN
|
|
Catalogs []string // Catalog names (bare names from input)
|
|
Group string // Optional RFC 9432 group property
|
|
COO string // Optional RFC 9432 change-of-ownership FQDN
|
|
ZoneFile string // file= property: zone data path for BIND config
|
|
DNSSEC bool // dnssec flag: adds dnssec-policy to BIND config
|
|
ZonesFile string // Input file path (for error messages)
|
|
Line int // Input line number (for error messages)
|
|
}
|
|
|
|
// CatalogMembers groups zone entries by catalog name.
|
|
type CatalogMembers map[string][]ZoneEntry
|
|
|
|
func parseInput(path string, cfg *Config) ([]ZoneEntry, CatalogMembers, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("opening input: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
var entries []ZoneEntry
|
|
scanner := bufio.NewScanner(f)
|
|
lineNum := 0
|
|
for scanner.Scan() {
|
|
lineNum++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
entry, err := parseLine(line, path, lineNum)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, nil, fmt.Errorf("reading %s: %w", path, err)
|
|
}
|
|
|
|
members, err := buildCatalogMembers(entries, cfg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return entries, members, nil
|
|
}
|
|
|
|
func parseLine(line, file string, lineNum int) (ZoneEntry, error) {
|
|
// Split on whitespace first, then handle comma separation within tokens
|
|
tokens := tokenize(line)
|
|
if len(tokens) < 2 {
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: expected zone name followed by at least one catalog name", file, lineNum)
|
|
}
|
|
|
|
entry := ZoneEntry{
|
|
Zone: normalizeFQDN(tokens[0]),
|
|
ZonesFile: file,
|
|
Line: lineNum,
|
|
}
|
|
|
|
for _, tok := range tokens[1:] {
|
|
if strings.Contains(tok, "=") {
|
|
key, value, _ := strings.Cut(tok, "=")
|
|
switch strings.ToLower(key) {
|
|
case "group":
|
|
if value == "" {
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: empty group value", file, lineNum)
|
|
}
|
|
entry.Group = value
|
|
case "coo":
|
|
if value == "" {
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: empty coo value", file, lineNum)
|
|
}
|
|
entry.COO = normalizeFQDN(value)
|
|
case "file":
|
|
if value == "" {
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: empty file value", file, lineNum)
|
|
}
|
|
entry.ZoneFile = value
|
|
case "dnssec":
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: dnssec is a flag, use without =", file, lineNum)
|
|
default:
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: unknown property %q", file, lineNum, key)
|
|
}
|
|
} else if tok == "dnssec" {
|
|
entry.DNSSEC = true
|
|
} else {
|
|
// Bare name = catalog assignment
|
|
entry.Catalogs = append(entry.Catalogs, tok)
|
|
}
|
|
}
|
|
|
|
if len(entry.Catalogs) == 0 {
|
|
return ZoneEntry{}, fmt.Errorf("%s:%d: no catalog assignment for zone %s", file, lineNum, entry.Zone)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
// tokenize splits a line on whitespace and commas, stripping empty tokens.
|
|
func tokenize(line string) []string {
|
|
return strings.Fields(strings.ReplaceAll(line, ",", " "))
|
|
}
|
|
|
|
func buildCatalogMembers(entries []ZoneEntry, cfg *Config) (CatalogMembers, error) {
|
|
members := make(CatalogMembers)
|
|
|
|
// Track duplicates: catalog -> zone -> line
|
|
seen := make(map[string]map[string]int)
|
|
|
|
for _, entry := range entries {
|
|
for _, catName := range entry.Catalogs {
|
|
if _, ok := cfg.Catalogs[catName]; !ok {
|
|
return nil, fmt.Errorf("%s:%d: unknown catalog %q", entry.ZonesFile, entry.Line, catName)
|
|
}
|
|
|
|
if seen[catName] == nil {
|
|
seen[catName] = make(map[string]int)
|
|
}
|
|
if prevLine, dup := seen[catName][entry.Zone]; dup {
|
|
return nil, fmt.Errorf("%s:%d: zone %s already assigned to catalog %q at line %d",
|
|
entry.ZonesFile, entry.Line, entry.Zone, catName, prevLine)
|
|
}
|
|
seen[catName][entry.Zone] = entry.Line
|
|
|
|
members[catName] = append(members[catName], entry)
|
|
}
|
|
}
|
|
|
|
return members, nil
|
|
}
|