package bind import ( "encoding/hex" "strings" "testing" ) func journalHeader(magic string, begin, end uint32) []byte { b := make([]byte, 32) copy(b, magic) put := func(off int, v uint32) { b[off] = byte(v >> 24) b[off+1] = byte(v >> 16) b[off+2] = byte(v >> 8) b[off+3] = byte(v) } put(16, begin) put(24, end) return b } func TestParseZoneSerial(t *testing.T) { bindDump := `$ORIGIN . $TTL 3600 ; 1 hour k8s.syd1.au.unkin.net IN SOA ns1.k8s.syd1.au.unkin.net. hostmaster.k8s.syd1.au.unkin.net. ( 16 ; serial 300 ; refresh (5 minutes) 60 ; retry (1 minute) 1209600 ; expire (2 weeks) 60 ; minimum (1 minute) ) ` cases := []struct { name string content string want int64 ok bool }{ {"bind dump", bindDump, 16, true}, {"seed", renderSeedZone("example.com", "10.0.0.1", 42), 42, true}, {"single line", "@ IN SOA ns1.example.com. hostmaster.example.com. 7 300 60 1209600 60\n", 7, true}, {"glued paren", "@ IN SOA ns. host. (9 300 60 1209600 60)\n", 9, true}, {"no soa", "$TTL 3600\nwww IN A 192.0.2.1\n", 0, false}, {"truncated", "@ IN SOA ns.\n", 0, false}, {"non numeric serial", "@ IN SOA ns. host. ( abc 300 )\n", 0, false}, } for _, c := range cases { got, ok := ParseZoneSerial(c.content) if got != c.want || ok != c.ok { t.Errorf("%s: ParseZoneSerial = (%d,%v) want (%d,%v)", c.name, got, ok, c.want, c.ok) } } } func TestParseJournalHeader(t *testing.T) { begin, end, ok := parseJournalHeader(journalHeader(";BIND LOG V9.2\n", 10, 16)) if !ok || begin != 10 || end != 16 { t.Errorf("V9.2 header = (%d,%d,%v) want (10,16,true)", begin, end, ok) } if _, _, ok := parseJournalHeader(journalHeader("not a journal\n", 10, 16)); ok { t.Error("bad magic should not parse") } if _, _, ok := parseJournalHeader([]byte(";BIND LOG V9.2\n")); ok { t.Error("truncated header should not parse") } } func TestParseZoneDiskState(t *testing.T) { out := strings.Join([]string{ "zonefile=1", "head<<", "@ IN SOA ns. host. ( 5 300 60 1209600 60 )", ">>head", "journal=1", "jnl=" + hex.EncodeToString(journalHeader(";BIND LOG V9.2\n", 3, 8)), "", }, "\n") st := parseZoneDiskState(out) if !st.ZoneFile || !st.ZoneSerialOK || st.ZoneSerial != 5 { t.Errorf("zone file state wrong: %+v", st) } if !st.Journal || !st.JournalOK || st.JournalBegin != 3 || st.JournalEnd != 8 { t.Errorf("journal state wrong: %+v", st) } empty := parseZoneDiskState("zonefile=0\njournal=0\n") if empty.ZoneFile || empty.Journal { t.Errorf("empty state wrong: %+v", empty) } } // applyPlan models what the pod filesystem looks like after the plan runs, so // a second PlanSeed can be checked for idempotence. func applyPlan(st ZoneDiskState, p SeedPlan) ZoneDiskState { if p.QuarantineJournal { st.Journal, st.JournalBegin, st.JournalEnd, st.JournalOK = false, 0, 0, false } if p.QuarantineZoneFile { st.ZoneFile, st.ZoneSerial, st.ZoneSerialOK = false, 0, false } if p.WriteSeed { st.ZoneFile, st.ZoneSerial, st.ZoneSerialOK = true, p.Serial, true } return st } func TestPlanSeedFreshInstall(t *testing.T) { p := PlanSeed(ZoneDiskState{}) if !p.WriteSeed || p.Serial != 1 { t.Fatalf("fresh install should seed at serial 1, got %+v", p) } if p.QuarantineZoneFile || p.QuarantineJournal { t.Errorf("fresh install should quarantine nothing, got %+v", p) } } func TestPlanSeedOrphanJournal(t *testing.T) { st := ZoneDiskState{Journal: true, JournalBegin: 10, JournalEnd: 16, JournalOK: true} p := PlanSeed(st) if !p.WriteSeed { t.Fatalf("orphan journal should still seed, got %+v", p) } if !p.QuarantineJournal || p.QuarantineZoneFile { t.Errorf("only the journal should be quarantined, got %+v", p) } if p.Serial <= 16 { t.Errorf("seed serial %d must exceed the journal end serial 16", p.Serial) } if p.QuarantineSuffix != ".orphaned-16" { t.Errorf("quarantine suffix should be deterministic, got %q", p.QuarantineSuffix) } } func TestPlanSeedFileRegressedBehindJournal(t *testing.T) { // The production failure: a skeleton at serial 1 left next to a journal at // serial 16, which BIND refuses to replay ("out of range"). st := ZoneDiskState{ ZoneFile: true, ZoneSerial: 1, ZoneSerialOK: true, Journal: true, JournalBegin: 10, JournalEnd: 16, JournalOK: true, } p := PlanSeed(st) if !p.WriteSeed || p.Serial <= 16 { t.Fatalf("reseed must land above the journal end serial, got %+v", p) } if !p.QuarantineJournal || !p.QuarantineZoneFile { t.Errorf("the unloadable pair should both be moved aside, got %+v", p) } after := applyPlan(st, p) if after.Journal { t.Error("journal should be gone after quarantine") } if next := PlanSeed(after); next != (SeedPlan{}) { t.Errorf("second reconcile should be a no-op, got %+v", next) } if after.ZoneSerial != p.Serial { t.Errorf("second reconcile changed the serial: %d want %d", after.ZoneSerial, p.Serial) } } func TestPlanSeedLeavesHealthyZoneAlone(t *testing.T) { cases := []struct { name string st ZoneDiskState }{ {"file and covering journal", ZoneDiskState{ ZoneFile: true, ZoneSerial: 16, ZoneSerialOK: true, Journal: true, JournalBegin: 10, JournalEnd: 20, JournalOK: true, }}, {"file at journal end", ZoneDiskState{ ZoneFile: true, ZoneSerial: 20, ZoneSerialOK: true, Journal: true, JournalBegin: 10, JournalEnd: 20, JournalOK: true, }}, {"file without journal", ZoneDiskState{ZoneFile: true, ZoneSerial: 16, ZoneSerialOK: true}}, {"unreadable journal header", ZoneDiskState{ ZoneFile: true, ZoneSerial: 16, ZoneSerialOK: true, Journal: true, }}, {"unparsable zone file", ZoneDiskState{ZoneFile: true}}, } for _, c := range cases { if p := PlanSeed(c.st); p != (SeedPlan{}) { t.Errorf("%s: live data must not be touched, got %+v", c.name, p) } } } func TestPlanSeedStaleJournalBehindFile(t *testing.T) { st := ZoneDiskState{ ZoneFile: true, ZoneSerial: 30, ZoneSerialOK: true, Journal: true, JournalBegin: 10, JournalEnd: 16, JournalOK: true, } p := PlanSeed(st) if p.WriteSeed || p.QuarantineZoneFile { t.Fatalf("a file ahead of its journal is live data, got %+v", p) } if !p.QuarantineJournal { t.Errorf("the unreplayable journal should be moved aside, got %+v", p) } if next := PlanSeed(applyPlan(st, p)); next != (SeedPlan{}) { t.Errorf("second reconcile should be a no-op, got %+v", next) } } func TestPlanSeedFreshInstallIdempotent(t *testing.T) { st := ZoneDiskState{} p := PlanSeed(st) after := applyPlan(st, p) if next := PlanSeed(after); next != (SeedPlan{}) { t.Fatalf("second reconcile of a fresh zone should be a no-op, got %+v", next) } if after.ZoneSerial != 1 { t.Errorf("serial reset on second reconcile: %d", after.ZoneSerial) } } func TestPlanSeedSerialWrap(t *testing.T) { st := ZoneDiskState{Journal: true, JournalBegin: 1 << 31, JournalEnd: 1<<32 - 1, JournalOK: true} if got := PlanSeed(st).Serial; got != 1 { t.Errorf("serial after wrap = %d want 1", got) } } func TestSeedZoneRoundTripsThroughParser(t *testing.T) { content := renderSeedZone("200.18.198.in-addr.arpa", "198.18.200.8", 17) got, ok := ParseZoneSerial(content) if !ok || got != 17 { t.Fatalf("seed zone serial = (%d,%v) want (17,true)", got, ok) } } func TestQuarantinePathsAreSuffixed(t *testing.T) { path := ZoneFilePath("example.com") if JournalPath(path) != path+".jnl" { t.Fatalf("journal path = %q", JournalPath(path)) } cmd := moveAside(JournalPath(path), ".orphaned-16") if !strings.Contains(cmd, "'"+path+".jnl.orphaned-16'") { t.Errorf("quarantine command should rename, not delete: %s", cmd) } if strings.Contains(cmd, "rm ") { t.Errorf("quarantine must never delete: %s", cmd) } }