mirror of
https://github.com/coredns/coredns.git
synced 2026-10-09 12:05:22 -04:00
* plugin/file: track zone mtime for reload_by_mtime Signed-off-by: git-jxj <65210887+git-jxj@users.noreply.github.com> * plugin/file: capture initial mtime before parsing Signed-off-by: git-jxj <65210887+git-jxj@users.noreply.github.com> * plugin/file: keep unloaded zones retryable after open error Signed-off-by: git-jxj <65210887+git-jxj@users.noreply.github.com> --------- Signed-off-by: git-jxj <65210887+git-jxj@users.noreply.github.com>
407 lines
11 KiB
Go
407 lines
11 KiB
Go
package file
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/coredns/coredns/plugin/test"
|
|
"github.com/coredns/coredns/plugin/transfer"
|
|
"github.com/coredns/coredns/request"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
func TestZoneReload(t *testing.T) {
|
|
fileName, rm, err := test.TempFile(".", reloadZoneTest)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create zone: %s", err)
|
|
}
|
|
defer rm()
|
|
reader, err := os.Open(fileName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open zone: %s", err)
|
|
}
|
|
z, err := Parse(reader, "miek.nl", fileName, 0)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse zone: %s", err)
|
|
}
|
|
|
|
z.ReloadInterval = 10 * time.Millisecond
|
|
z.Reload(&transfer.Transfer{})
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
ctx := context.TODO()
|
|
r := new(dns.Msg)
|
|
r.SetQuestion("miek.nl", dns.TypeSOA)
|
|
state := request.Request{W: &test.ResponseWriter{}, Req: r}
|
|
if _, _, _, res := z.Lookup(ctx, state, "miek.nl."); res != Success {
|
|
t.Fatalf("Failed to lookup, got %d", res)
|
|
}
|
|
|
|
r = new(dns.Msg)
|
|
r.SetQuestion("miek.nl", dns.TypeNS)
|
|
state = request.Request{W: &test.ResponseWriter{}, Req: r}
|
|
if _, _, _, res := z.Lookup(ctx, state, "miek.nl."); res != Success {
|
|
t.Fatalf("Failed to lookup, got %d", res)
|
|
}
|
|
|
|
rrs, err := z.ApexIfDefined() // all apex records.
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) != 5 {
|
|
t.Fatalf("Expected 5 RRs, got %d", len(rrs))
|
|
}
|
|
if err := os.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil {
|
|
t.Fatalf("Failed to write new zone data: %s", err)
|
|
}
|
|
for start := time.Now(); time.Since(start) < 2*time.Second; {
|
|
rrs, err = z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) == 3 {
|
|
break
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
if len(rrs) != 3 {
|
|
t.Fatalf("Expected 3 RRs, got %d", len(rrs))
|
|
}
|
|
}
|
|
|
|
func TestZoneReloadSOAChange(t *testing.T) {
|
|
_, err := Parse(strings.NewReader(reloadZoneTest), "miek.nl.", "stdin", 1460175181)
|
|
if err == nil {
|
|
t.Fatalf("Zone should not have been re-parsed")
|
|
}
|
|
}
|
|
|
|
func TestZoneReloadSOAOrigin(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "db.example.org")
|
|
z, err := Parse(strings.NewReader(dbRelative), "example.org.", fileName, -1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeApex, beforeTree := z.snapshot()
|
|
updated := strings.Replace(dbRelative, " 3 3600", " 4 3600", 1)
|
|
updated = strings.Replace(updated, "192.0.2.1", "192.0.2.2", 1)
|
|
invalid := strings.Replace(updated, "@ 500 IN SOA", "child 500 IN SOA", 1)
|
|
if err := os.WriteFile(fileName, []byte(invalid), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
z.ReloadInterval = time.Second
|
|
if err := z.Reload(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer z.OnShutdown()
|
|
|
|
time.Sleep(2 * time.Second)
|
|
synctest.Wait()
|
|
apex, tree := z.snapshot()
|
|
if apex.SOA != beforeApex.SOA || tree != beforeTree {
|
|
t.Fatal("invalid reload replaced the last valid zone")
|
|
}
|
|
|
|
if err := os.WriteFile(fileName, []byte(updated), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(time.Second)
|
|
synctest.Wait()
|
|
apex, tree = z.snapshot()
|
|
if apex.SOA.Hdr.Name != "example.org." || apex.SOA.Serial != 4 || tree == beforeTree {
|
|
t.Fatalf("corrected zone was not reloaded: %v", apex.SOA)
|
|
}
|
|
r := new(dns.Msg)
|
|
r.SetQuestion("foo.example.org.", dns.TypeA)
|
|
state := request.Request{W: &test.ResponseWriter{}, Req: r}
|
|
answer, _, _, result := z.Lookup(context.Background(), state, state.Name())
|
|
if result != Success || len(answer) != 1 || answer[0].String() != "foo.example.org.\t500\tIN\tA\t192.0.2.2" {
|
|
t.Fatalf("expected updated A record, got result %v, answer %v", result, answer)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestZoneReloadByMtime(t *testing.T) {
|
|
// Test 1: Basic mtime trigger - file modification should trigger reload
|
|
t.Run("BasicMtimeTrigger", func(t *testing.T) {
|
|
z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
|
|
defer cleanup()
|
|
|
|
// Wait for initial load to complete
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
// Verify initial content (5 records)
|
|
rrs, err := z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) != 5 {
|
|
t.Fatalf("Expected 5 initial RRs, got %d", len(rrs))
|
|
}
|
|
|
|
// Modify the zone file (this changes mtime)
|
|
if err := os.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil {
|
|
t.Fatalf("Failed to write new zone data: %s", err)
|
|
}
|
|
|
|
// Poll until reload is observed (fixed sleeps race under -race, esp. on Windows).
|
|
for start := time.Now(); time.Since(start) < 2*time.Second; {
|
|
rrs, err = z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) == 3 {
|
|
break
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
if len(rrs) != 3 {
|
|
t.Fatalf("Expected 3 RRs after reload, got %d", len(rrs))
|
|
}
|
|
})
|
|
|
|
// Test 2: No reload when mtime unchanged
|
|
t.Run("NoReloadWhenMtimeUnchanged", func(t *testing.T) {
|
|
z, _, cleanup := prepareMtimeZone(t, reloadZoneTest)
|
|
defer cleanup()
|
|
|
|
// Wait for initial load
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
// Record initial SOA serial
|
|
initialSerial := z.SOASerialIfDefined()
|
|
if initialSerial == -1 {
|
|
t.Fatal("Failed to get initial SOA serial")
|
|
}
|
|
|
|
// Record initial record count
|
|
rrs, err := z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
initialCount := len(rrs)
|
|
|
|
// Wait for multiple reload intervals WITHOUT modifying the file
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify no reload occurred
|
|
currentSerial := z.SOASerialIfDefined()
|
|
if currentSerial != initialSerial {
|
|
t.Fatalf("SOA serial changed unexpectedly: %d -> %d", initialSerial, currentSerial)
|
|
}
|
|
|
|
rrs, err = z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) != initialCount {
|
|
t.Fatalf("Record count changed unexpectedly: %d -> %d", initialCount, len(rrs))
|
|
}
|
|
})
|
|
|
|
// Test 3: Content verification after reload
|
|
t.Run("ContentVerificationAfterReload", func(t *testing.T) {
|
|
z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
|
|
defer cleanup()
|
|
|
|
ctx := context.TODO()
|
|
|
|
// Query initial content
|
|
r := new(dns.Msg)
|
|
r.SetQuestion("miek.nl", dns.TypeNS)
|
|
state := request.Request{W: &test.ResponseWriter{}, Req: r}
|
|
|
|
records, _, _, res := z.Lookup(ctx, state, "miek.nl.")
|
|
if res != Success {
|
|
t.Fatalf("Failed to lookup initial NS records, got %d", res)
|
|
}
|
|
|
|
// Initial zone has 4 NS records
|
|
if len(records) != 4 {
|
|
t.Fatalf("Expected 4 initial NS records, got %d", len(records))
|
|
}
|
|
|
|
// Modify to new zone content (only 2 NS records)
|
|
if err := os.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil {
|
|
t.Fatalf("Failed to write new zone data: %s", err)
|
|
}
|
|
|
|
// Poll until reload is observed (fixed sleeps race under -race, esp. on Windows).
|
|
for start := time.Now(); time.Since(start) < 2*time.Second; {
|
|
records, _, _, res = z.Lookup(ctx, state, "miek.nl.")
|
|
if res != Success {
|
|
t.Fatalf("Failed to lookup reloaded NS records, got %d", res)
|
|
}
|
|
if len(records) == 2 {
|
|
break
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
|
|
// Reloaded zone has 2 NS records
|
|
if len(records) != 2 {
|
|
t.Fatalf("Expected 2 reloaded NS records, got %d", len(records))
|
|
}
|
|
|
|
// Verify the actual NS record names match the new zone
|
|
nsNames := make([]string, len(records))
|
|
for i, rr := range records {
|
|
nsNames[i] = rr.(*dns.NS).Ns
|
|
}
|
|
|
|
expectedNS := []string{"ext.ns.whyscream.net.", "omval.tednet.nl."}
|
|
for i, expected := range expectedNS {
|
|
if nsNames[i] != expected {
|
|
t.Errorf("Expected NS record %d to be %s, got %s", i, expected, nsNames[i])
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test 4: File deleted/missing during reload
|
|
t.Run("FileMissingDuringReload", func(t *testing.T) {
|
|
z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
|
|
defer cleanup()
|
|
|
|
// Wait for initial load
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
// Verify initial content is loaded
|
|
rrs, err := z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
initialCount := len(rrs)
|
|
|
|
// Delete the zone file
|
|
if err := os.Remove(fileName); err != nil {
|
|
t.Fatalf("Failed to remove zone file: %s", err)
|
|
}
|
|
|
|
// Wait for reload interval (reload should fail gracefully)
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
// Verify zone still serves old content (didn't crash)
|
|
rrs, err = z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) != initialCount {
|
|
t.Fatalf("Zone content changed unexpectedly after file deletion: %d -> %d", initialCount, len(rrs))
|
|
}
|
|
|
|
// Verify DNS queries still work
|
|
ctx := context.TODO()
|
|
r := new(dns.Msg)
|
|
r.SetQuestion("miek.nl", dns.TypeSOA)
|
|
state := request.Request{W: &test.ResponseWriter{}, Req: r}
|
|
|
|
_, _, _, res := z.Lookup(ctx, state, "miek.nl.")
|
|
if res != Success {
|
|
t.Fatalf("Zone should still serve queries after file deletion, got result %d", res)
|
|
}
|
|
})
|
|
|
|
// Test 5: A successful reload advances the mtime baseline.
|
|
t.Run("MtimeBaselineAdvancesAfterReload", func(t *testing.T) {
|
|
z, fileName, cleanup := prepareMtimeZone(t, reloadZoneTest)
|
|
defer cleanup()
|
|
|
|
if err := os.WriteFile(fileName, []byte(reloadZone2Test), 0644); err != nil {
|
|
t.Fatalf("Failed to write new zone data: %s", err)
|
|
}
|
|
|
|
var rrs []dns.RR
|
|
for start := time.Now(); time.Since(start) < 2*time.Second; {
|
|
var err error
|
|
rrs, err = z.ApexIfDefined()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rrs) == 3 {
|
|
break
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
if len(rrs) != 3 {
|
|
t.Fatalf("Expected 3 RRs after reload, got %d", len(rrs))
|
|
}
|
|
|
|
fi, err := os.Stat(fileName)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat zone: %s", err)
|
|
}
|
|
z.RLock()
|
|
mtime := z.file_mtime
|
|
z.RUnlock()
|
|
if !mtime.Equal(fi.ModTime()) {
|
|
t.Fatalf("file mtime = %s after reload, want %s", mtime, fi.ModTime())
|
|
}
|
|
})
|
|
}
|
|
|
|
// prepareMtimeZone creates a zone with mtime-based reload enabled.
|
|
func prepareMtimeZone(t *testing.T, content string) (*Zone, string, func()) {
|
|
t.Helper()
|
|
fileName, rm, err := test.TempFile(".", content)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create zone: %s", err)
|
|
}
|
|
// A rewrite immediately after creating the file can retain the same mtime
|
|
// on filesystems with coarse timestamp resolution. Start with an older
|
|
// mtime so the rewrite is always observable by the reload loop.
|
|
initialMtime := time.Now().Add(-time.Minute)
|
|
if err := os.Chtimes(fileName, initialMtime, initialMtime); err != nil {
|
|
rm()
|
|
t.Fatalf("Failed to set initial zone mtime: %s", err)
|
|
}
|
|
|
|
reader, err := os.Open(fileName)
|
|
if err != nil {
|
|
rm()
|
|
t.Fatalf("Failed to open zone: %s", err)
|
|
}
|
|
z, err := Parse(reader, "miek.nl", fileName, 0)
|
|
reader.Close()
|
|
if err != nil {
|
|
rm()
|
|
t.Fatalf("Failed to parse zone: %s", err)
|
|
}
|
|
|
|
fi, err := os.Stat(fileName)
|
|
if err != nil {
|
|
rm()
|
|
t.Fatalf("Failed to stat zone: %s", err)
|
|
}
|
|
|
|
z.ReloadInterval = 10 * time.Millisecond
|
|
z.ReloadByMtime = true
|
|
// Parse does not set file_mtime unless ReloadByMtime is already enabled.
|
|
// Seed it so the reload loop only opens the file when mtime changes.
|
|
z.file_mtime = fi.ModTime()
|
|
z.Reload(&transfer.Transfer{})
|
|
|
|
return z, fileName, func() {
|
|
z.OnShutdown()
|
|
rm()
|
|
}
|
|
}
|
|
|
|
const reloadZoneTest = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400
|
|
miek.nl. 1627 IN NS ext.ns.whyscream.net.
|
|
miek.nl. 1627 IN NS omval.tednet.nl.
|
|
miek.nl. 1627 IN NS linode.atoom.net.
|
|
miek.nl. 1627 IN NS ns-ext.nlnetlabs.nl.
|
|
`
|
|
|
|
const reloadZone2Test = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175182 14400 3600 604800 14400
|
|
miek.nl. 1627 IN NS ext.ns.whyscream.net.
|
|
miek.nl. 1627 IN NS omval.tednet.nl.
|
|
`
|