reconcile zone apex NS off the pod IP placeholder
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

Add BindZone.spec.nameservers and sync the apex NS RRset on every reconcile.
This commit is contained in:
2026-09-26 18:27:11 +10:00
parent 03df5d56b4
commit 4a41cbc427
14 changed files with 346 additions and 39 deletions
+41 -12
View File
@@ -26,36 +26,65 @@ func (e *Executor) ZoneExists(ctx context.Context, namespace, pod, zone, view st
return err == nil
}
// renderSeedZone renders a minimal loadable zone (SOA + apex NS + glue). The
// apex NS is the in-zone name ns1, and a glue A record pointing at primaryIP is
// included so BIND's check-integrity accepts the zone (an in-zone NS without an
// address record is a load error).
func renderSeedZone(zone, primaryIP string, serial int64) string {
// renderSeedZone renders a minimal loadable zone (SOA + apex NS). nameservers
// are the names published in the apex NS RRset; when empty the in-zone name ns1
// is used. A glue A pointing at primaryIP is emitted only for a nameserver that
// falls inside the zone, because BIND refuses to load a zone whose in-zone NS
// has no address record. Out-of-zone nameservers therefore keep pod IPs out of
// the zone file entirely.
func renderSeedZone(zone, primaryIP string, nameservers []string, serial int64) string {
origin := dot(zone)
ns := "ns1." + origin
ns := make([]string, 0, len(nameservers))
for _, n := range nameservers {
ns = append(ns, dot(n))
}
if len(ns) == 0 {
ns = []string{"ns1." + origin}
}
// Short refresh/retry so a secondary that misses a NOTIFY (e.g. its pod IP
// changed and the primary's also-notify was briefly stale) still converges
// in minutes, not the hour a 3600s refresh would impose. minimum is the
// negative-cache TTL: keep it low so a stale-secondary NXDOMAIN does not
// stick in downstream resolvers for long. NOTIFY (also-notify on the
// primary) remains the fast path; these are the fallback.
return fmt.Sprintf(`$TTL 3600
var b strings.Builder
fmt.Fprintf(&b, `$TTL 3600
@ IN SOA %s hostmaster.%s (
%d ; serial
300 ; refresh
60 ; retry
1209600 ; expire
60 ) ; minimum
@ IN NS %s
ns1 IN A %s
`, ns, origin, serial, ns, primaryIP)
`, ns[0], origin, serial)
for _, n := range ns {
fmt.Fprintf(&b, "@ IN NS %s\n", n)
}
for _, n := range ns {
if owner, ok := InZoneOwner(n, zone); ok {
fmt.Fprintf(&b, "%s IN A %s\n", owner, primaryIP)
}
}
return b.String()
}
// InZoneOwner reports whether name sits inside zone, and if so returns its owner
// name relative to the apex ("@" for the apex itself).
func InZoneOwner(name, zone string) (string, bool) {
name, origin := dot(name), dot(zone)
switch {
case name == origin:
return "@", true
case strings.HasSuffix(name, "."+origin):
return strings.TrimSuffix(name, "."+origin), true
}
return "", false
}
// EnsureSeedZone makes path loadable without discarding live data: it probes
// the zone file and journal, moves aside whatever cannot load, and writes a
// skeleton only when there is nothing to preserve. Every caller that needs a
// zone database file on disk goes through here.
func (e *Executor) EnsureSeedZone(ctx context.Context, namespace, pod, zone, path, primaryIP string) error {
func (e *Executor) EnsureSeedZone(ctx context.Context, namespace, pod, zone, path, primaryIP string, nameservers []string) error {
state, err := e.ZoneDiskState(ctx, namespace, pod, path)
if err != nil {
return err
@@ -67,7 +96,7 @@ func (e *Executor) EnsureSeedZone(ctx context.Context, namespace, pod, zone, pat
if !plan.WriteSeed {
return e.Quarantine(ctx, namespace, pod, path, plan)
}
content := renderSeedZone(zone, primaryIP, plan.Serial)
content := renderSeedZone(zone, primaryIP, nameservers, plan.Serial)
cmd := []string{"sh", "-c", seedScript(path, plan, len(content))}
if out, err := e.Exec(ctx, namespace, pod, cmd, content); err != nil {
return fmt.Errorf("seed zone %s: %w (out: %s)", zone, err, out)