129 lines
5.1 KiB
Go
129 lines
5.1 KiB
Go
package bind
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// ZoneFilePath returns the on-pod path of a zone database file.
|
|
func ZoneFilePath(zone string) string {
|
|
return fmt.Sprintf("%s/zones/db.%s", DataDir, strings.TrimSuffix(zone, "."))
|
|
}
|
|
|
|
// CatalogFilePath returns the on-pod path of a catalog zone database file.
|
|
func CatalogFilePath(zone string) string {
|
|
return fmt.Sprintf("%s/catalog/db.%s", DataDir, strings.TrimSuffix(zone, "."))
|
|
}
|
|
|
|
// ZoneExists reports whether a zone is currently loaded on the pod.
|
|
func (e *Executor) ZoneExists(ctx context.Context, namespace, pod, zone, view string) bool {
|
|
args := []string{"zonestatus", zone}
|
|
if view != "" {
|
|
args = append(args, "in", view)
|
|
}
|
|
_, err := e.Rndc(ctx, namespace, pod, args...)
|
|
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 {
|
|
origin := dot(zone)
|
|
ns := "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
|
|
@ 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)
|
|
}
|
|
|
|
// 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 {
|
|
state, err := e.ZoneDiskState(ctx, namespace, pod, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plan := PlanSeed(state)
|
|
if plan.Blocked != "" {
|
|
return fmt.Errorf("seed zone %s: %s", zone, plan.Blocked)
|
|
}
|
|
if !plan.WriteSeed {
|
|
return e.Quarantine(ctx, namespace, pod, path, plan)
|
|
}
|
|
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. shellScript aborts the run at the first failure, so
|
|
// no step can install the skeleton over data an earlier step failed to preserve.
|
|
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("n=$(wc -c < %s | tr -d ' \\n')", tmp),
|
|
// Compared as strings: an unmeasurable size is empty, not a number, and
|
|
// a numeric test would exit 2 there and be swallowed by the if.
|
|
fmt.Sprintf("if [ \"$n\" != '%d' ]; then rm -f %s; exit 1; fi", size, tmp),
|
|
}
|
|
if plan.QuarantineZoneFile {
|
|
cmds = append(cmds, moveAside(path, plan.QuarantineSuffix))
|
|
}
|
|
if plan.QuarantineJournal {
|
|
cmds = append(cmds, moveAside(JournalPath(path), plan.QuarantineSuffix))
|
|
}
|
|
return shellScript(append(cmds, fmt.Sprintf("mv -- %s %s", tmp, q))...)
|
|
}
|
|
|
|
// 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 {
|
|
hash := catalogHash(memberZone)
|
|
owner := fmt.Sprintf("%s.zones.%s", hash, dot(catalogZone))
|
|
updates := []RecordUpdate{{
|
|
FQDN: owner,
|
|
Type: "PTR",
|
|
TTL: 3600,
|
|
Values: []string{dot(memberZone)},
|
|
}}
|
|
return e.NSUpdate(ctx, namespace, pod, catalogZone, creds, updates)
|
|
}
|
|
|
|
// RemoveCatalogMember deregisters a member zone from a catalog zone.
|
|
func (e *Executor) RemoveCatalogMember(ctx context.Context, namespace, pod, catalogZone, memberZone string, creds TSIGCreds) error {
|
|
hash := catalogHash(memberZone)
|
|
owner := fmt.Sprintf("%s.zones.%s", hash, dot(catalogZone))
|
|
updates := []RecordUpdate{{FQDN: owner, Type: "PTR", Delete: true}}
|
|
return e.NSUpdate(ctx, namespace, pod, catalogZone, creds, updates)
|
|
}
|