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.
This commit is contained in:
2026-09-19 23:14:26 +10:00
parent 0065268372
commit 6730b8bcb2
+30 -15
View File
@@ -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 {