Reload hook (#1445)

* Add reload directive

* gofmt

* Fix default jitter and error message

* remove unneeded call to NextArg, add a couple negative setup tests

* Review feedback
This commit is contained in:
John Belamaric
2018-01-27 05:42:57 -05:00
committed by Miek Gieben
parent 80050766fb
commit 0b35d4d28f
7 changed files with 237 additions and 0 deletions

View File

@@ -11,6 +11,7 @@ package dnsserver
// care what plugin above them are doing. // care what plugin above them are doing.
var Directives = []string{ var Directives = []string{
"tls", "tls",
"reload",
"nsid", "nsid",
"root", "root",
"bind", "bind",

View File

@@ -26,6 +26,7 @@ import (
_ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/nsid"
_ "github.com/coredns/coredns/plugin/pprof" _ "github.com/coredns/coredns/plugin/pprof"
_ "github.com/coredns/coredns/plugin/proxy" _ "github.com/coredns/coredns/plugin/proxy"
_ "github.com/coredns/coredns/plugin/reload"
_ "github.com/coredns/coredns/plugin/reverse" _ "github.com/coredns/coredns/plugin/reverse"
_ "github.com/coredns/coredns/plugin/rewrite" _ "github.com/coredns/coredns/plugin/rewrite"
_ "github.com/coredns/coredns/plugin/root" _ "github.com/coredns/coredns/plugin/root"

View File

@@ -20,6 +20,7 @@
# log:log # log:log
tls:tls tls:tls
reload:reload
nsid:nsid nsid:nsid
root:root root:root
bind:bind bind:bind

58
plugin/reload/README.md Normal file
View File

@@ -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
}
~~~

65
plugin/reload/reload.go Normal file
View File

@@ -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
}

72
plugin/reload/setup.go Normal file
View File

@@ -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
)

View File

@@ -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)
}
}