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:
+30
-15
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user