middleware/auto: add (#333)

Add auto-load middleware that automatically picks up zones.

Every X seconds it will scan for new zones.
Add tests and documentation.

Make 'make test' use -race.
This commit is contained in:
Miek Gieben 2016-10-17 18:37:56 +01:00 committed by GitHub
parent 2eafe3ee94
commit d536272201
19 changed files with 838 additions and 18 deletions

View file

@ -23,11 +23,11 @@ deps:
.PHONY: test .PHONY: test
test: deps test: deps
go test $(TEST_VERBOSE) ./... go test -race $(TEST_VERBOSE) ./...
.PHONY: testk8s .PHONY: testk8s
testk8s: deps testk8s: deps
go test $(TEST_VERBOSE) -tags=k8s -run 'TestKubernetes' ./test ./middleware/kubernetes/... go test -race $(TEST_VERBOSE) -tags=k8s -run 'TestKubernetes' ./test ./middleware/kubernetes/...
.PHONY: coverage .PHONY: coverage
coverage: deps coverage: deps

View file

@ -5,7 +5,8 @@ import (
// plug in the server // plug in the server
_ "github.com/miekg/coredns/core/dnsserver" _ "github.com/miekg/coredns/core/dnsserver"
// plug in the standard directives // plug in the standard directives (sorted)
_ "github.com/miekg/coredns/middleware/auto"
_ "github.com/miekg/coredns/middleware/bind" _ "github.com/miekg/coredns/middleware/bind"
_ "github.com/miekg/coredns/middleware/cache" _ "github.com/miekg/coredns/middleware/cache"
_ "github.com/miekg/coredns/middleware/chaos" _ "github.com/miekg/coredns/middleware/chaos"

View file

@ -89,6 +89,7 @@ var directives = []string{
"dnssec", "dnssec",
"file", "file",
"auto",
"secondary", "secondary",
"etcd", "etcd",
"kubernetes", "kubernetes",

61
middleware/auto/README.md Normal file
View file

@ -0,0 +1,61 @@
# auto
*auto* enables serving zone data from an RFC 1035-style master file which is automatically picked
up from disk.
The *auto* middleware is used for an "old-style" DNS server. It serves from a preloaded file that exists
on disk. If the zone file contains signatures (i.e. is signed, i.e. DNSSEC) correct DNSSEC answers
are returned. Only NSEC is supported! If you use this setup *you* are responsible for resigning the
zonefile. New zones or changed zone are automatically picked up from disk.
## Syntax
~~~
auto [ZONES...] {
directory DIR [REGEXP ORIGIN_TEMPLATE [TIMEOUT]]
}
~~~
**ZONES** zones it should be authoritative for. If empty, the zones from the configuration block
are used.
* `directory` loads zones from the speficied **DIR**. If a file name matches **REGEXP** it will be
used to extract the origin. **ORIGIN_TEMPLATE** will be used as a template for the origin. Strings
like `{<number>}` are replaced with the respective matches in the file name, i.e. `{1}` is the
first match, `{2}` is the second, etc.. The default is: `db\.(.*) {1}` e.g. from a file with the
name `db.example.com`, the extracted origin will be `example.com`. **TIMEOUT** specifies how often
CoreDNS should scan the directory, the default is every 60 seconds. This value is in seconds.
The minimum value is 1 second.
All directives from the *file* middleware are supported. Note that *auto* will load all zones found,
even though the directive might only receive queries for a specific zone. I.e:
~~~
auto example.org {
directory /etc/coredns/zones
}
~~~
Will happily pick up a zone for `example.COM`, except it will never be queried, because the *auto*
directive only is authoritative for `example.ORG`.
## Examples
Load `org` domains from `/etc/coredns/zones/org` and allow transfers to the internet, but send
notifies to 10.240.1.1
~~~
auto org {
directory /etc/coredns/zones/org
transfer to *
transfer to 10.240.1.1
}
~~~
Load `org` domains from `/etc/coredns/zones/org` and looks for file names as `www.db.example.org`,
where `example.org` is the origin. Scan every 45 seconds.
~~~
auto org {
directory /etc/coredns/zones/org www\.db\.(.*) {1} 45
}
~~~

99
middleware/auto/auto.go Normal file
View file

@ -0,0 +1,99 @@
// Package auto implements an on-the-fly loading file backend.
package auto
import (
"errors"
"regexp"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/file"
"github.com/miekg/coredns/request"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
type (
// Auto holds the zones and the loader configuration for automatically loading zones.
Auto struct {
Next middleware.Handler
*Zones
loader
}
loader struct {
directory string
template string
re *regexp.Regexp
// In the future this should be something like ZoneMeta that contains all this stuff.
transferTo []string
noReload bool
duration time.Duration
}
)
// ServeDNS implements the middleware.Handle interface.
func (a Auto) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if state.QClass() != dns.ClassINET {
return dns.RcodeServerFailure, errors.New("can only deal with ClassINET")
}
qname := state.Name()
// TODO(miek): match the qname better in the map
// Precheck with the origins, i.e. are we allowed to looks here.
zone := middleware.Zones(a.Zones.Origins()).Matches(qname)
if zone == "" {
if a.Next != nil {
return a.Next.ServeDNS(ctx, w, r)
}
return dns.RcodeServerFailure, errors.New("no next middleware found")
}
// Now the real zone.
zone = middleware.Zones(a.Zones.Names()).Matches(qname)
a.Zones.RLock()
z, ok := a.Zones.Z[zone]
a.Zones.RUnlock()
if !ok {
return a.Next.ServeDNS(ctx, w, r)
}
if z == nil {
return dns.RcodeServerFailure, nil
}
if state.QType() == dns.TypeAXFR || state.QType() == dns.TypeIXFR {
xfr := file.Xfr{Zone: z}
return xfr.ServeDNS(ctx, w, r)
}
answer, ns, extra, result := z.Lookup(qname, state.QType(), state.Do())
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Answer, m.Ns, m.Extra = answer, ns, extra
switch result {
case file.Success:
case file.NoData:
case file.NameError:
m.Rcode = dns.RcodeNameError
case file.Delegation:
m.Authoritative = false
case file.ServerFailure:
return dns.RcodeServerFailure, nil
}
state.SizeAndDo(m)
m, _ = state.Scrub(m)
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}

20
middleware/auto/regexp.go Normal file
View file

@ -0,0 +1,20 @@
package auto
// rewriteToExpand rewrites our template string to one that we can give to regexp.ExpandString. This basically
// involves prefixing any '{' with a '$'.
func rewriteToExpand(s string) string {
// Pretty dumb at the moment, every { will get a $ prefixed.
// Also wasteful as we build the string with +=. This is OKish
// as we do this during config parsing.
copy := ""
for _, c := range s {
if c == '{' {
copy += "$"
}
copy += string(c)
}
return copy
}

View file

@ -0,0 +1,20 @@
package auto
import "testing"
func TestRewriteToExpand(t *testing.T) {
tests := []struct {
in string
expected string
}{
{in: "", expected: ""},
{in: "{1}", expected: "${1}"},
{in: "{1", expected: "${1"},
}
for i, tc := range tests {
got := rewriteToExpand(tc.in)
if got != tc.expected {
t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got)
}
}
}

150
middleware/auto/setup.go Normal file
View file

@ -0,0 +1,150 @@
package auto
import (
"log"
"os"
"path"
"regexp"
"strconv"
"time"
"github.com/miekg/coredns/core/dnsserver"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/file"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("auto", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
a, err := autoParse(c)
if err != nil {
return middleware.Error("auto", err)
}
walkChan := make(chan bool)
c.OnStartup(func() error {
err := a.Zones.Walk(a.loader)
if err != nil {
return err
}
go func() {
ticker := time.NewTicker(a.loader.duration)
for {
select {
case <-walkChan:
return
case <-ticker.C:
a.Zones.Walk(a.loader)
}
}
}()
return nil
})
c.OnShutdown(func() error {
close(walkChan)
return nil
})
dnsserver.GetConfig(c).AddMiddleware(func(next middleware.Handler) middleware.Handler {
a.Next = next
return a
})
return nil
}
func autoParse(c *caddy.Controller) (Auto, error) {
var a = Auto{
loader: loader{template: "${1}", re: regexp.MustCompile(`db\.(.*)`), duration: time.Duration(60 * time.Second)},
Zones: &Zones{},
}
config := dnsserver.GetConfig(c)
for c.Next() {
if c.Val() == "auto" {
// auto [ZONES...]
a.Zones.origins = make([]string, len(c.ServerBlockKeys))
copy(a.Zones.origins, c.ServerBlockKeys)
args := c.RemainingArgs()
if len(args) > 0 {
a.Zones.origins = args
}
for i := range a.Zones.origins {
a.Zones.origins[i] = middleware.Host(a.Zones.origins[i]).Normalize()
}
for c.NextBlock() {
switch c.Val() {
case "directory": // directory DIR [REGEXP [TEMPLATE] [DURATION]]
if !c.NextArg() {
return a, c.ArgErr()
}
a.loader.directory = c.Val()
if !path.IsAbs(a.loader.directory) && config.Root != "" {
a.loader.directory = path.Join(config.Root, a.loader.directory)
}
_, err := os.Stat(a.loader.directory)
if err != nil {
if os.IsNotExist(err) {
log.Printf("[WARNING] Directory does not exist: %s", a.loader.directory)
} else {
return a, c.Errf("Unable to access root path '%s': %v", a.loader.directory, err)
}
}
// regexp
if c.NextArg() {
a.loader.re, err = regexp.Compile(c.Val())
if err != nil {
return a, err
}
if a.loader.re.NumSubexp() == 0 {
return a, c.Errf("Need at least one sub expression")
}
}
// template
if c.NextArg() {
a.loader.template = rewriteToExpand(c.Val())
}
// template
if c.NextArg() {
i, err := strconv.Atoi(c.Val())
if err != nil {
return a, err
}
if i < 1 {
i = 1
}
a.loader.duration = time.Duration(i) * time.Second
}
case "no_reload":
a.loader.noReload = true
default:
t, _, e := file.TransferParse(c, false)
if e != nil {
return a, e
}
a.loader.transferTo = t
}
}
}
}
return a, nil
}

View file

@ -0,0 +1,81 @@
package auto
import (
"testing"
"github.com/mholt/caddy"
)
func TestAutoParse(t *testing.T) {
tests := []struct {
inputFileRules string
shouldErr bool
expectedDirectory string
expectedTempl string
expectedRe string
expectedTo string
}{
{
`auto example.org {
directory /tmp
transfer to 127.0.0.1
}`,
false, "/tmp", "${1}", `db\.(.*)`, "127.0.0.1:53",
},
{
`auto {
directory /tmp
}`,
false, "/tmp", "${1}", `db\.(.*)`, "",
},
{
`auto {
directory /tmp (.*) bliep
}`,
false, "/tmp", "bliep", `(.*)`, "",
},
// errors
{
`auto example.org {
directory
}`,
true, "", "${1}", `db\.(.*)`, "",
},
{
`auto example.org {
directory /tmp * {1}
}`,
true, "", "${1}", ``, "",
},
{
`auto example.org {
directory /tmp .* {1}
}`,
true, "", "${1}", ``, "",
},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.inputFileRules)
a, err := autoParse(c)
if err == nil && test.shouldErr {
t.Fatalf("Test %d expected errors, but got no error", i)
} else if err != nil && !test.shouldErr {
t.Fatalf("Test %d expected no errors, but got '%v'", i, err)
} else if !test.shouldErr {
if a.loader.directory != test.expectedDirectory {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.loader.directory)
}
if a.loader.template != test.expectedTempl {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.loader.template)
}
if a.loader.re.String() != test.expectedRe {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.loader.re)
}
if test.expectedTo != "" && a.loader.transferTo[0] != test.expectedTo {
t.Fatalf("Test %d expected %v, got %v", i, test.expectedTo, a.loader.transferTo[0])
}
}
}
}

96
middleware/auto/walk.go Normal file
View file

@ -0,0 +1,96 @@
package auto
import (
"log"
"os"
"path"
"path/filepath"
"regexp"
"github.com/miekg/coredns/middleware/file"
"github.com/miekg/dns"
)
// Walk will recursively walk of the file under l.directory and adds the one that match l.re.
func (z *Zones) Walk(l loader) error {
// TODO(miek): should add something so that we don't stomp on each other.
toDelete := make(map[string]bool)
for _, n := range z.Names() {
toDelete[n] = true
}
filepath.Walk(l.directory, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
match, origin := matches(l.re, info.Name(), l.template)
if !match {
return nil
}
if _, ok := z.Z[origin]; ok {
// we already have this zone
toDelete[origin] = false
return nil
}
reader, err := os.Open(path)
if err != nil {
log.Printf("[WARNING] Opening %s failed: %s", path, err)
return nil
}
zo, err := file.Parse(reader, origin, path)
if err != nil {
// Parse barfs warning by itself...
return nil
}
zo.NoReload = l.noReload
zo.TransferTo = l.transferTo
z.Insert(zo, origin)
zo.Notify()
log.Printf("[INFO] Inserting zone `%s' from: %s", origin, path)
toDelete[origin] = false
return nil
})
for origin, ok := range toDelete {
if !ok {
continue
}
z.Delete(origin)
log.Printf("[INFO] Deleting zone `%s'", origin)
}
return nil
}
// matches matches re to filename, if is is a match, the subexpression will be used to expand
// template to an origin. When match is true that origin is returned. Origin is fully qualified.
func matches(re *regexp.Regexp, filename, template string) (match bool, origin string) {
base := path.Base(filename)
matches := re.FindStringSubmatchIndex(base)
if matches == nil {
return false, ""
}
by := re.ExpandString(nil, template, base, matches)
if by == nil {
return false, ""
}
origin = dns.Fqdn(string(by))
return true, origin
}

View file

@ -0,0 +1,72 @@
package auto
import (
"io/ioutil"
"log"
"os"
"path"
"regexp"
"testing"
)
var dbFiles = []string{"db.example.org", "aa.example.org"}
const zoneContent = `; testzone
@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600
NS a.iana-servers.net.
NS b.iana-servers.net.
www IN A 127.0.0.1
`
func TestWalk(t *testing.T) {
log.SetOutput(ioutil.Discard)
tempdir, err := createFiles()
if err != nil {
if tempdir != "" {
os.RemoveAll(tempdir)
}
t.Fatal(err)
}
defer os.RemoveAll(tempdir)
ldr := loader{
directory: tempdir,
re: regexp.MustCompile(`db\.(.*)`),
template: `${1}`,
}
z := &Zones{}
z.Walk(ldr)
// db.example.org and db.example.com should be here (created in createFiles)
for _, name := range []string{"example.com.", "example.org."} {
if _, ok := z.Z[name]; !ok {
t.Errorf("%s should have been added", name)
}
}
}
func createFiles() (string, error) {
dir, err := ioutil.TempDir(os.TempDir(), "coredns")
if err != nil {
return dir, err
}
for _, name := range dbFiles {
if err := ioutil.WriteFile(path.Join(dir, name), []byte(zoneContent), 0644); err != nil {
return dir, err
}
}
// symlinks
if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "db.example.com")); err != nil {
return dir, err
}
if err = os.Symlink(path.Join(dir, "db.example.org"), path.Join(dir, "aa.example.com")); err != nil {
return dir, err
}
return dir, nil
}

View file

@ -0,0 +1,48 @@
package auto
import (
"io/ioutil"
"log"
"os"
"path"
"regexp"
"testing"
)
func TestWatcher(t *testing.T) {
log.SetOutput(ioutil.Discard)
tempdir, err := createFiles()
if err != nil {
if tempdir != "" {
os.RemoveAll(tempdir)
}
t.Fatal(err)
}
defer os.RemoveAll(tempdir)
ldr := loader{
directory: tempdir,
re: regexp.MustCompile(`db\.(.*)`),
template: `${1}`,
}
z := &Zones{}
z.Walk(ldr)
// example.org and example.com should exist
if x := len(z.Z["example.org."].All()); x != 4 {
t.Fatalf("expected 4 RRs, got %d", x)
}
if x := len(z.Z["example.com."].All()); x != 4 {
t.Fatalf("expected 4 RRs, got %d", x)
}
// Now remove one file, rescan and see if it's gone.
if err := os.Remove(path.Join(tempdir, "db.example.com")); err != nil {
t.Fatal(err)
}
z.Walk(ldr)
}

76
middleware/auto/zone.go Normal file
View file

@ -0,0 +1,76 @@
// Package auto implements a on-the-fly loading file backend.
package auto
import (
"sync"
"github.com/miekg/coredns/middleware/file"
)
// Zones maps zone names to a *Zone. This keep track of what we zones we have loaded at
// any one time.
type Zones struct {
Z map[string]*file.Zone // A map mapping zone (origin) to the Zone's data.
names []string // All the keys from the map Z as a string slice.
origins []string // Any origins from the server block.
sync.RWMutex
}
// Names returns the names from z.
func (z *Zones) Names() []string {
z.RLock()
n := z.names
z.RUnlock()
return n
}
// Origins returns the origins from z.
func (z *Zones) Origins() []string {
// doesn't need locking, because there aren't multiple Go routines accessing it.
return z.origins
}
// Zones returns a zone with origin name from z, nil when not found.
func (z *Zones) Zones(name string) *file.Zone {
z.RLock()
zo := z.Z[name]
z.RUnlock()
return zo
}
// Insert inserts a new zone into z. If zo.NoReload is false, the
// reload goroutine is started.
func (z *Zones) Insert(zo *file.Zone, name string) {
z.Lock()
if z.Z == nil {
z.Z = make(map[string]*file.Zone)
}
z.Z[name] = zo
z.names = append(z.names, name)
zo.Reload()
z.Unlock()
}
// Delete removes the zone named name from z. It also stop the the zone's reload goroutine.
func (z *Zones) Delete(name string) {
z.Lock()
if zo, ok := z.Z[name]; ok && !zo.NoReload {
zo.ReloadShutdown <- true
}
delete(z.Z, name)
// just regenerate Names (might be bad if you have a lot of zones...)
z.names = []string{}
for n := range z.Z {
z.names = append(z.names, n)
}
z.Unlock()
}

View file

@ -22,7 +22,6 @@ func fakeStubServerExampleNet(t *testing.T) (*dns.Server, string) {
} }
// add handler for example.net // add handler for example.net
dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) { dns.HandleFunc("example.net.", func(w dns.ResponseWriter, r *dns.Msg) {
t.Logf("writing response for example.net.")
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(r) m.SetReply(r)
m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")} m.Answer = []dns.RR{test.A("example.net. 86400 IN A 93.184.216.34")}

View file

@ -22,8 +22,8 @@ type (
// Zones maps zone names to a *Zone. // Zones maps zone names to a *Zone.
Zones struct { Zones struct {
Z map[string]*Zone Z map[string]*Zone // A map mapping zone (origin) to the Zone's data
Names []string Names []string // All the keys from the map Z as a string slice.
} }
) )
@ -35,6 +35,7 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
return dns.RcodeServerFailure, errors.New("can only deal with ClassINET") return dns.RcodeServerFailure, errors.New("can only deal with ClassINET")
} }
qname := state.Name() qname := state.Name()
// TODO(miek): match the qname better in the map
zone := middleware.Zones(f.Zones.Names).Matches(qname) zone := middleware.Zones(f.Zones.Names).Matches(qname)
if zone == "" { if zone == "" {
if f.Next != nil { if f.Next != nil {
@ -49,6 +50,8 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
if z == nil { if z == nil {
return dns.RcodeServerFailure, nil return dns.RcodeServerFailure, nil
} }
// This is only for when we are a secondary zones.
if r.Opcode == dns.OpcodeNotify { if r.Opcode == dns.OpcodeNotify {
if z.isNotify(state) { if z.isNotify(state) {
m := new(dns.Msg) m := new(dns.Msg)

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/miekg/coredns/middleware/test" "github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -28,7 +29,7 @@ func TestZoneReload(t *testing.T) {
t.Fatalf("failed to parse zone: %s", err) t.Fatalf("failed to parse zone: %s", err)
} }
z.Reload(nil) z.Reload()
if _, _, _, res := z.Lookup("miek.nl.", dns.TypeSOA, false); res != Success { if _, _, _, res := z.Lookup("miek.nl.", dns.TypeSOA, false); res != Success {
t.Fatalf("failed to lookup, got %d", res) t.Fatalf("failed to lookup, got %d", res)

View file

@ -33,7 +33,7 @@ func setup(c *caddy.Controller) error {
if len(z.TransferTo) > 0 { if len(z.TransferTo) > 0 {
z.Notify() z.Notify()
} }
z.Reload(nil) z.Reload()
}) })
return nil return nil
}) })
@ -99,7 +99,7 @@ func fileParse(c *caddy.Controller) (Zones, error) {
case "no_reload": case "no_reload":
noReload = true noReload = true
} }
// discard from, here, maybe check and show log when we do?
for _, origin := range origins { for _, origin := range origins {
if t != nil { if t != nil {
z[origin].TransferTo = append(z[origin].TransferTo, t...) z[origin].TransferTo = append(z[origin].TransferTo, t...)
@ -113,8 +113,6 @@ func fileParse(c *caddy.Controller) (Zones, error) {
} }
// TransferParse parses transfer statements: 'transfer to [address...]'. // TransferParse parses transfer statements: 'transfer to [address...]'.
// Exported so secondary can use this as well. For the `file` middleware transfer from does
// not make sense; make this an error.
func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) { func TransferParse(c *caddy.Controller, secondary bool) (tos, froms []string, err error) {
what := c.Val() what := c.Val()
if !c.NextArg() { if !c.NextArg() {

View file

@ -29,7 +29,7 @@ type Zone struct {
NoReload bool NoReload bool
reloadMu sync.RWMutex reloadMu sync.RWMutex
// TODO: shutdown watcher channel ReloadShutdown chan bool
} }
// Apex contains the apex records of a zone: SOA, NS and their potential signatures. // Apex contains the apex records of a zone: SOA, NS and their potential signatures.
@ -42,7 +42,13 @@ type Apex struct {
// NewZone returns a new zone. // NewZone returns a new zone.
func NewZone(name, file string) *Zone { func NewZone(name, file string) *Zone {
z := &Zone{origin: dns.Fqdn(name), file: path.Clean(file), Tree: &tree.Tree{}, Expired: new(bool)} z := &Zone{
origin: dns.Fqdn(name),
file: path.Clean(file),
Tree: &tree.Tree{},
Expired: new(bool),
ReloadShutdown: make(chan bool),
}
*z.Expired = false *z.Expired = false
return z return z
} }
@ -138,7 +144,7 @@ func (z *Zone) All() []dns.RR {
} }
// Reload reloads a zone when it is changed on disk. If z.NoRoload is true, no reloading will be done. // Reload reloads a zone when it is changed on disk. If z.NoRoload is true, no reloading will be done.
func (z *Zone) Reload(shutdown chan bool) error { func (z *Zone) Reload() error {
if z.NoReload { if z.NoReload {
return nil return nil
} }
@ -156,7 +162,7 @@ func (z *Zone) Reload(shutdown chan bool) error {
for { for {
select { select {
case event := <-watcher.Events: case event := <-watcher.Events:
if path.Clean(event.Name) == z.file { if event.Op == fsnotify.Write && path.Clean(event.Name) == z.file {
reader, err := os.Open(z.file) reader, err := os.Open(z.file)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err) log.Printf("[ERROR] Failed to open `%s' for `%s': %v", z.file, z.origin, err)
@ -176,7 +182,7 @@ func (z *Zone) Reload(shutdown chan bool) error {
log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin) log.Printf("[INFO] Successfully reloaded zone `%s'", z.origin)
z.Notify() z.Notify()
} }
case <-shutdown: case <-z.ReloadShutdown:
watcher.Close() watcher.Close()
return return
} }

88
test/auto_test.go Normal file
View file

@ -0,0 +1,88 @@
package test
import (
"io/ioutil"
"log"
"os"
"path"
"testing"
"time"
"github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/coredns/request"
"github.com/miekg/dns"
)
func TestAuto(t *testing.T) {
tmpdir, err := ioutil.TempDir(os.TempDir(), "coredns")
if err != nil {
t.Fatal(err)
}
corefile := `org:0 {
auto {
directory ` + tmpdir + ` db\.(.*) {1} 1
}
}
`
i, err := CoreDNSServer(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
udp, _ := CoreDNSServerPorts(i, 0)
if udp == "" {
t.Fatalf("Could not get UDP listening port")
}
defer i.Stop()
log.SetOutput(ioutil.Discard)
p := proxy.New([]string{udp})
state := request.Request{W: &test.ResponseWriter{}, Req: new(dns.Msg)}
resp, err := p.Lookup(state, "www.example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if resp.Rcode != dns.RcodeServerFailure {
t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode)
}
// Write db.example.org to get example.org.
if err = ioutil.WriteFile(path.Join(tmpdir, "db.example.org"), []byte(zoneContent), 0644); err != nil {
t.Fatal(err)
}
time.Sleep(1100 * time.Millisecond) // wait for it to be picked up
resp, err = p.Lookup(state, "www.example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if len(resp.Answer) != 1 {
t.Fatalf("Expected 1 RR in the answer section, got %d", len(resp.Answer))
}
// Remove db.example.org again.
os.Remove(path.Join(tmpdir, "db.example.org"))
time.Sleep(1100 * time.Millisecond) // wait for it to be picked up
resp, err = p.Lookup(state, "www.example.org.", dns.TypeA)
if err != nil {
t.Fatal("Expected to receive reply, but didn't")
}
if resp.Rcode != dns.RcodeServerFailure {
t.Fatalf("Expected reply to be a SERVFAIL, got %d", resp.Rcode)
}
}
const zoneContent = `; testzone
@ IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082534 7200 3600 1209600 3600
NS a.iana-servers.net.
NS b.iana-servers.net.
www IN A 127.0.0.1
`