diff options
-rw-r--r-- | core/dnsserver/zdirectives.go | 1 | ||||
-rw-r--r-- | core/plugin/zplugin.go | 1 | ||||
-rw-r--r-- | plugin.cfg | 1 | ||||
-rw-r--r-- | plugin/reload/README.md | 58 | ||||
-rw-r--r-- | plugin/reload/reload.go | 65 | ||||
-rw-r--r-- | plugin/reload/setup.go | 72 | ||||
-rw-r--r-- | plugin/reload/setup_test.go | 39 |
7 files changed, 237 insertions, 0 deletions
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index ba8873936..f0017e82c 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -11,6 +11,7 @@ package dnsserver // care what plugin above them are doing. var Directives = []string{ "tls", + "reload", "nsid", "root", "bind", diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index c893a6139..a81c8ef9b 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -26,6 +26,7 @@ import ( _ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/pprof" _ "github.com/coredns/coredns/plugin/proxy" + _ "github.com/coredns/coredns/plugin/reload" _ "github.com/coredns/coredns/plugin/reverse" _ "github.com/coredns/coredns/plugin/rewrite" _ "github.com/coredns/coredns/plugin/root" diff --git a/plugin.cfg b/plugin.cfg index d3efb3889..2b34faa63 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -20,6 +20,7 @@ # log:log tls:tls +reload:reload nsid:nsid root:root bind:bind diff --git a/plugin/reload/README.md b/plugin/reload/README.md new file mode 100644 index 000000000..64e5b1d93 --- /dev/null +++ b/plugin/reload/README.md @@ -0,0 +1,58 @@ +# reload + +## Name + +*reload* - allows automatic reload of a changed Corefile + +## Description + +This plugin periodically checks if the Corefile has changed by reading +it and calculating its MD5 checksum. If the file has changed, it reloads +CoreDNS with the new Corefile. This eliminates the need to send a SIGHUP +or SIGUSR1 after changing the Corefile. + +The reloads are graceful - you should not see any loss of service when the +reload happens. Even if the new Corefile has an error, CoreDNS will continue +to run the old config and an error message will be printed to the log. + +In some environments (for example, Kubernetes), there may be many CoreDNS +instances that started very near the same time and all share a common +Corefile. To prevent these all from reloading at the same time, some +jitter is added to the reload check interval. This is jitter from the +perspective of multiple CoreDNS instances; each instance still checks on a +regular interval, but all of these instances will have their reloads spread +out across the jitter duration. This isn't strictly necessary given that the +reloads are graceful, and can be disabled by setting the jitter to `0s`. + +Jitter is re-calculated whenever the Corefile is reloaded. + +## Syntax + +~~~ txt +reload [INTERVAL] [JITTER] +~~~ + +* The plugin will check for changes every **INTERVAL**, subject to +/- the **JITTER** duration +* **INTERVAL** and **JITTER** are Golang (durations)[https://golang.org/pkg/time/#ParseDuration] +* Default **INTERVAL** is 30s, default **JITTER** is 15s +* If **JITTER** is more than half of **INTERVAL**, it will be set to half of **INTERVAL** + +## Examples + +Check with the default intervals: + +~~~ corefile +. { + reload + erratic +} +~~~ + +Check every 10 seconds (jitter is automatically set to 10 / 2 = 5 in this case): + +~~~ corefile +. { + reload 10s + erratic +} +~~~ diff --git a/plugin/reload/reload.go b/plugin/reload/reload.go new file mode 100644 index 000000000..e469b4526 --- /dev/null +++ b/plugin/reload/reload.go @@ -0,0 +1,65 @@ +package reload + +import ( + "crypto/md5" + "log" + "time" + + "github.com/mholt/caddy" +) + +// reload periodically checks if the Corefile has changed, and reloads if so + +type reload struct { + instance *caddy.Instance + interval time.Duration + sum [md5.Size]byte + stopped bool + quit chan bool +} + +func hook(event caddy.EventName, info interface{}) error { + if event != caddy.InstanceStartupEvent { + return nil + } + + // if reload is removed from the Corefile, then the hook + // is still registered but setup is never called again + // so we need a flag to tell us not to reload + if r.stopped { + return nil + } + + // this should be an instance. ok to panic if not + r.instance = info.(*caddy.Instance) + r.sum = md5.Sum(r.instance.Caddyfile().Body()) + + go func() { + tick := time.NewTicker(r.interval) + + for { + select { + case <-tick.C: + corefile, err := caddy.LoadCaddyfile(r.instance.Caddyfile().ServerType()) + if err != nil { + continue + } + s := md5.Sum(corefile.Body()) + if s != r.sum { + _, err := r.instance.Restart(corefile) + if err != nil { + log.Printf("[ERROR] Corefile changed but reload failed: %s\n", err) + continue + } + // we are done, this hook gets called again with new instance + r.stopped = true + return + } + case <-r.quit: + return + } + } + }() + + return nil +} diff --git a/plugin/reload/setup.go b/plugin/reload/setup.go new file mode 100644 index 000000000..af6fe1334 --- /dev/null +++ b/plugin/reload/setup.go @@ -0,0 +1,72 @@ +package reload + +import ( + "math/rand" + "sync" + "time" + + "github.com/coredns/coredns/plugin" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("reload", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +var r *reload +var once sync.Once + +func setup(c *caddy.Controller) error { + c.Next() // 'reload' + args := c.RemainingArgs() + + if len(args) > 2 { + return plugin.Error("reload", c.ArgErr()) + } + + i := defaultInterval + if len(args) > 0 { + d, err := time.ParseDuration(args[0]) + if err != nil { + return err + } + i = d + } + + j := defaultJitter + if len(args) > 1 { + d, err := time.ParseDuration(args[1]) + if err != nil { + return err + } + j = d + } + + if j > i/2 { + j = i / 2 + } + + jitter := time.Duration(rand.Int63n(j.Nanoseconds()) - (j.Nanoseconds() / 2)) + i = i + jitter + + r = &reload{interval: i, quit: make(chan bool)} + once.Do(func() { + caddy.RegisterEventHook("reload", hook) + }) + + c.OnFinalShutdown(func() error { + r.quit <- true + return nil + }) + + return nil +} + +const ( + defaultInterval = 30 * time.Second + defaultJitter = 15 * time.Second +) diff --git a/plugin/reload/setup_test.go b/plugin/reload/setup_test.go new file mode 100644 index 000000000..3c488bf2b --- /dev/null +++ b/plugin/reload/setup_test.go @@ -0,0 +1,39 @@ +package reload + +import ( + "testing" + + "github.com/mholt/caddy" +) + +func TestSetupReload(t *testing.T) { + c := caddy.NewTestController("dns", `reload`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s 2s`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `reload 10s 5s foo`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } +} |