Files
catz/input.go
Ask Bjørn Hansen 0eddb9fcfe Add --bind-conf flag for BIND domains.conf generation
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.
2026-03-28 11:15:06 -07:00

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
}