From 6730b8bcb29ece0b767b4715bd649f53f96b2d04 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 19 Sep 2026 23:14:26 +1000 Subject: [PATCH] Stage and install the seed zone file in one exec Quarantine and write were separate round-trips, so a failure between them left the PVC with no zone data and the next reconcile reseeded at serial 1. The write also truncated the destination in place, leaving a torn file that PlanSeed refuses to touch. --- internal/bind/seed.go | 45 ++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/internal/bind/seed.go b/internal/bind/seed.go index 353e97a..2a3ee76 100644 --- a/internal/bind/seed.go +++ b/internal/bind/seed.go @@ -54,8 +54,7 @@ ns1 IN A %s // 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: writeSeedZone is unexported so -// the destructive write cannot be reached without a plan. +// zone database file on disk goes through here. func (e *Executor) EnsureSeedZone(ctx context.Context, namespace, pod, zone, path, primaryIP string) error { state, err := e.ZoneDiskState(ctx, namespace, pod, path) if err != nil { @@ -65,27 +64,43 @@ func (e *Executor) EnsureSeedZone(ctx context.Context, namespace, pod, zone, pat if plan.Blocked != "" { return fmt.Errorf("seed zone %s: %s", zone, plan.Blocked) } - if err := e.Quarantine(ctx, namespace, pod, path, plan); err != nil { - return err - } if !plan.WriteSeed { - return nil + return e.Quarantine(ctx, namespace, pod, path, plan) } - return e.writeSeedZone(ctx, namespace, pod, zone, path, primaryIP, plan.Serial) -} - -// writeSeedZone writes a seed zone file to path, creating parent directories. -// It overwrites any existing file unconditionally. -func (e *Executor) writeSeedZone(ctx context.Context, namespace, pod, zone, path, primaryIP string, serial int64) error { - content := renderSeedZone(zone, primaryIP, serial) - q := shellQuote(path) - cmd := []string{"sh", "-c", fmt.Sprintf("mkdir -p \"$(dirname %s)\" && cat > %s", q, q)} + content := renderSeedZone(zone, primaryIP, 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) } return nil } +// seedTempSuffix names the staging file, a sibling of the zone file so the +// install is a same-filesystem rename. +const seedTempSuffix = ".seed-tmp" + +// seedScript stages the skeleton, checks it arrived whole, then quarantines and +// installs it in that order. Doing all of it in one exec keeps an interrupted +// seed from leaving the PVC with no zone data, which the next reconcile would +// read as a fresh install and reseed at serial 1; the rename means a torn write +// is never visible at path. +func seedScript(path string, plan SeedPlan, size int) string { + q, tmp := shellQuote(path), shellQuote(path+seedTempSuffix) + cmds := []string{ + fmt.Sprintf("mkdir -p \"$(dirname %s)\"", q), + fmt.Sprintf("cat > %s", tmp), + // A stdin stream cut mid-transfer gives cat a short file and exit 0. + fmt.Sprintf("if [ \"$(wc -c < %s | tr -d ' \\n')\" -ne %d ]; then rm -f %s; exit 1; fi", tmp, size, tmp), + } + if plan.QuarantineZoneFile { + cmds = append(cmds, moveAside(path, plan.QuarantineSuffix)) + } + if plan.QuarantineJournal { + cmds = append(cmds, moveAside(JournalPath(path), plan.QuarantineSuffix)) + } + return strings.Join(append(cmds, fmt.Sprintf("mv -- %s %s", tmp, q)), "\n") +} + // AddCatalogMember registers a member zone in a catalog zone by adding the // catalog PTR record, so secondaries auto-provision it. func (e *Executor) AddCatalogMember(ctx context.Context, namespace, pod, catalogZone, memberZone string, creds TSIGCreds) error {