mirror of
https://github.com/coredns/coredns.git
synced 2026-05-25 19:30:23 -04:00
plugin/file: trigger reload of zones based on mtime (#8085)
* Added fs.FileInfo.ModTime() based reload feature Signed-off-by: Endre Szabo <git@end.re> * Updated the plugin documentation. Signed-off-by: Endre Szabo <git@end.re> --------- Signed-off-by: Endre Szabo <git@end.re>
This commit is contained in:
@@ -27,6 +27,7 @@ If you want to round-robin A and AAAA responses look at the *loadbalance* plugin
|
|||||||
~~~
|
~~~
|
||||||
file DBFILE [ZONES... ] {
|
file DBFILE [ZONES... ] {
|
||||||
reload DURATION
|
reload DURATION
|
||||||
|
reload_by_mtime
|
||||||
fallthrough [ZONES...]
|
fallthrough [ZONES...]
|
||||||
}
|
}
|
||||||
~~~
|
~~~
|
||||||
@@ -34,6 +35,8 @@ file DBFILE [ZONES... ] {
|
|||||||
* `reload` interval to perform a reload of the zone if the SOA version changes. Default is one minute.
|
* `reload` interval to perform a reload of the zone if the SOA version changes. Default is one minute.
|
||||||
Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds
|
Value of `0` means to not scan for changes and reload. For example, `30s` checks the zonefile every 30 seconds
|
||||||
and reloads the zone when serial changes.
|
and reloads the zone when serial changes.
|
||||||
|
* `reload_by_mtime` if set, decision to reload the zone will be based on the zone file modification time,
|
||||||
|
instead of change in the SOA serial.
|
||||||
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
|
* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
|
||||||
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
|
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
|
||||||
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only
|
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/coredns/coredns/plugin"
|
"github.com/coredns/coredns/plugin"
|
||||||
"github.com/coredns/coredns/plugin/pkg/fall"
|
"github.com/coredns/coredns/plugin/pkg/fall"
|
||||||
@@ -148,6 +149,15 @@ func Parse(f io.Reader, origin, fileName string, serial int64) (*Zone, error) {
|
|||||||
zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName)
|
zp := dns.NewZoneParser(f, dns.Fqdn(origin), fileName)
|
||||||
zp.SetIncludeAllowed(true)
|
zp.SetIncludeAllowed(true)
|
||||||
z := NewZone(origin, fileName)
|
z := NewZone(origin, fileName)
|
||||||
|
|
||||||
|
if z.ReloadByMtime {
|
||||||
|
fi, err := os.Stat(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to stat file %q with error %v", fileName, err)
|
||||||
|
}
|
||||||
|
z.file_mtime = fi.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
seenSOA := false
|
seenSOA := false
|
||||||
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
for rr, ok := zp.Next(); ok; rr, ok = zp.Next() {
|
||||||
if !seenSOA {
|
if !seenSOA {
|
||||||
|
|||||||
@@ -20,13 +20,26 @@ func (z *Zone) Reload(t *transfer.Transfer) error {
|
|||||||
select {
|
select {
|
||||||
case <-tick.C:
|
case <-tick.C:
|
||||||
zFile := z.File()
|
zFile := z.File()
|
||||||
|
serial := z.SOASerialIfDefined()
|
||||||
|
|
||||||
|
if z.ReloadByMtime {
|
||||||
|
fi, err := os.Stat(zFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to stat zone %q in %q: %v", z.origin, zFile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !fi.ModTime().After(z.file_mtime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
serial = 0 // force reload of the zone
|
||||||
|
}
|
||||||
|
|
||||||
reader, err := os.Open(filepath.Clean(zFile))
|
reader, err := os.Open(filepath.Clean(zFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to open zone %q in %q: %v", z.origin, zFile, err)
|
log.Errorf("Failed to open zone %q in %q: %v", z.origin, zFile, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
serial := z.SOASerialIfDefined()
|
|
||||||
zone, err := Parse(reader, z.origin, zFile, serial)
|
zone, err := Parse(reader, z.origin, zFile, serial)
|
||||||
reader.Close()
|
reader.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -77,6 +77,253 @@ func TestZoneReloadSOAChange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestZoneReloadByMtime(t *testing.T) {
|
||||||
|
// Test 1: Basic mtime trigger - file modification should trigger reload
|
||||||
|
t.Run("BasicMtimeTrigger", func(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)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
|
||||||
|
// Enable mtime-based reload
|
||||||
|
z.ReloadInterval = 10 * time.Millisecond
|
||||||
|
z.ReloadByMtime = true
|
||||||
|
z.Reload(&transfer.Transfer{})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for reload to trigger
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify reload occurred (3 records now)
|
||||||
|
rrs, err = z.ApexIfDefined()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
|
||||||
|
// Enable mtime-based reload
|
||||||
|
z.ReloadInterval = 10 * time.Millisecond
|
||||||
|
z.ReloadByMtime = true
|
||||||
|
z.Reload(&transfer.Transfer{})
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
|
||||||
|
// Enable mtime-based reload
|
||||||
|
z.ReloadInterval = 10 * time.Millisecond
|
||||||
|
z.ReloadByMtime = true
|
||||||
|
z.Reload(&transfer.Transfer{})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for reload
|
||||||
|
time.Sleep(30 * time.Millisecond)
|
||||||
|
|
||||||
|
// Query new content
|
||||||
|
records, _, _, res = z.Lookup(ctx, state, "miek.nl.")
|
||||||
|
if res != Success {
|
||||||
|
t.Fatalf("Failed to lookup reloaded NS records, got %d", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
reader.Close()
|
||||||
|
|
||||||
|
// Enable mtime-based reload
|
||||||
|
z.ReloadInterval = 10 * time.Millisecond
|
||||||
|
z.ReloadByMtime = true
|
||||||
|
z.Reload(&transfer.Transfer{})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const reloadZoneTest = `miek.nl. 1627 IN SOA linode.atoom.net. miek.miek.nl. 1460175181 14400 3600 604800 14400
|
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 ext.ns.whyscream.net.
|
||||||
miek.nl. 1627 IN NS omval.tednet.nl.
|
miek.nl. 1627 IN NS omval.tednet.nl.
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ func fileParse(c *caddy.Controller) (Zones, fall.F, error) {
|
|||||||
|
|
||||||
var openErr error
|
var openErr error
|
||||||
reload := 1 * time.Minute
|
reload := 1 * time.Minute
|
||||||
|
reload_by_mtime := false
|
||||||
|
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
// file db.file [zones...]
|
// file db.file [zones...]
|
||||||
@@ -131,6 +132,8 @@ func fileParse(c *caddy.Controller) (Zones, fall.F, error) {
|
|||||||
return Zones{}, fall, plugin.Error("file", err)
|
return Zones{}, fall, plugin.Error("file", err)
|
||||||
}
|
}
|
||||||
reload = d
|
reload = d
|
||||||
|
case "reload_by_mtime":
|
||||||
|
reload_by_mtime = true
|
||||||
case "upstream":
|
case "upstream":
|
||||||
// remove soon
|
// remove soon
|
||||||
c.RemainingArgs()
|
c.RemainingArgs()
|
||||||
@@ -143,6 +146,7 @@ func fileParse(c *caddy.Controller) (Zones, fall.F, error) {
|
|||||||
for i := range origins {
|
for i := range origins {
|
||||||
z[origins[i]].ReloadInterval = reload
|
z[origins[i]].ReloadInterval = reload
|
||||||
z[origins[i]].Upstream = upstream.New()
|
z[origins[i]].Upstream = upstream.New()
|
||||||
|
z[origins[i]].ReloadByMtime = reload_by_mtime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import (
|
|||||||
|
|
||||||
// Zone is a structure that contains all data related to a DNS zone.
|
// Zone is a structure that contains all data related to a DNS zone.
|
||||||
type Zone struct {
|
type Zone struct {
|
||||||
origin string
|
origin string
|
||||||
origLen int
|
origLen int
|
||||||
file string
|
file string
|
||||||
|
file_mtime time.Time
|
||||||
*tree.Tree
|
*tree.Tree
|
||||||
Apex
|
Apex
|
||||||
Expired bool
|
Expired bool
|
||||||
@@ -28,6 +29,7 @@ type Zone struct {
|
|||||||
TransferFrom []string
|
TransferFrom []string
|
||||||
|
|
||||||
ReloadInterval time.Duration
|
ReloadInterval time.Duration
|
||||||
|
ReloadByMtime bool
|
||||||
reloadShutdown chan bool
|
reloadShutdown chan bool
|
||||||
|
|
||||||
Upstream *upstream.Upstream // Upstream for looking up external names during the resolution process.
|
Upstream *upstream.Upstream // Upstream for looking up external names during the resolution process.
|
||||||
|
|||||||
Reference in New Issue
Block a user