diff options
Diffstat (limited to 'middleware')
46 files changed, 2362 insertions, 112 deletions
diff --git a/middleware/bind/README.md b/middleware/bind/README.md new file mode 100644 index 000000000..ad23b6153 --- /dev/null +++ b/middleware/bind/README.md @@ -0,0 +1,21 @@ +# bind + +bind overrides the host to which the server should bind. Normally, the listener binds to the +wildcard host. However, you may force the listener to bind to another IP instead. This +directive accepts only an address, not a port. + +## Syntax + +~~~ txt +bind address +~~~ + +address is the IP address to bind to. + +## Examples + +To make your socket accessible only to that machine, bind to IP 127.0.0.1 (localhost): + +~~~ txt +bind 127.0.0.1 +~~~ diff --git a/middleware/bind/bind.go b/middleware/bind/bind.go new file mode 100644 index 000000000..ac27c993b --- /dev/null +++ b/middleware/bind/bind.go @@ -0,0 +1,10 @@ +package bind + +import "github.com/mholt/caddy" + +func init() { + caddy.RegisterPlugin("bind", caddy.Plugin{ + ServerType: "dns", + Action: setupBind, + }) +} diff --git a/middleware/bind/bind_test.go b/middleware/bind/bind_test.go new file mode 100644 index 000000000..d61741a02 --- /dev/null +++ b/middleware/bind/bind_test.go @@ -0,0 +1,30 @@ +package bind + +import ( + "testing" + + "github.com/miekg/coredns/core/dnsserver" + + "github.com/mholt/caddy" +) + +func TestSetupBind(t *testing.T) { + c := caddy.NewTestController("dns", `bind 1.2.3.4`) + err := setupBind(c) + if err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + cfg := dnsserver.GetConfig(c) + if got, want := cfg.ListenHost, "1.2.3.4"; got != want { + t.Errorf("Expected the config's ListenHost to be %s, was %s", want, got) + } +} + +func TestBindAddress(t *testing.T) { + c := caddy.NewTestController("dns", `bind 1.2.3.bla`) + err := setupBind(c) + if err == nil { + t.Fatalf("Expected errors, but got none") + } +} diff --git a/middleware/bind/setup.go b/middleware/bind/setup.go new file mode 100644 index 000000000..c08098b5d --- /dev/null +++ b/middleware/bind/setup.go @@ -0,0 +1,23 @@ +package bind + +import ( + "fmt" + "net" + + "github.com/miekg/coredns/core/dnsserver" + + "github.com/mholt/caddy" +) + +func setupBind(c *caddy.Controller) error { + config := dnsserver.GetConfig(c) + for c.Next() { + if !c.Args(&config.ListenHost) { + return c.ArgErr() + } + } + if net.ParseIP(config.ListenHost) == nil { + return fmt.Errorf("not a valid IP address: %s", config.ListenHost) + } + return nil +} diff --git a/middleware/cache/setup.go b/middleware/cache/setup.go new file mode 100644 index 000000000..ab7f423f2 --- /dev/null +++ b/middleware/cache/setup.go @@ -0,0 +1,66 @@ +package cache + +import ( + "strconv" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("cache", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +// Cache sets up the root file path of the server. +func setup(c *caddy.Controller) error { + ttl, zones, err := cacheParse(c) + if err != nil { + return err + } + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return NewCache(ttl, zones, next) + }) + + return nil +} + +func cacheParse(c *caddy.Controller) (int, []string, error) { + var ( + err error + ttl int + origins []string + ) + + for c.Next() { + if c.Val() == "cache" { + // cache [ttl] [zones..] + origins = make([]string, len(c.ServerBlockKeys)) + copy(origins, c.ServerBlockKeys) + args := c.RemainingArgs() + if len(args) > 0 { + origins = args + // first args may be just a number, then it is the ttl, if not it is a zone + t := origins[0] + ttl, err = strconv.Atoi(t) + if err == nil { + origins = origins[1:] + if len(origins) == 0 { + // There was *only* the ttl, revert back to server block + copy(origins, c.ServerBlockKeys) + } + } + } + + for i, _ := range origins { + origins[i] = middleware.Host(origins[i]).Normalize() + } + return ttl, origins, nil + } + } + return 0, nil, nil +} diff --git a/middleware/chaos/setup.go b/middleware/chaos/setup.go new file mode 100644 index 000000000..8bdb3053e --- /dev/null +++ b/middleware/chaos/setup.go @@ -0,0 +1,50 @@ +package chaos + +import ( + "github.com/miekg/coredns/core/dnsserver" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("chaos", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + version, authors, err := chaosParse(c) + if err != nil { + return err + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return Chaos{Next: next, Version: version, Authors: authors} + }) + + return nil +} + +func chaosParse(c *caddy.Controller) (string, map[string]bool, error) { + version := "" + authors := make(map[string]bool) + + for c.Next() { + args := c.RemainingArgs() + if len(args) == 0 { + return defaultVersion, nil, nil + } + if len(args) == 1 { + return args[0], nil, nil + } + version = args[0] + for _, a := range args[1:] { + authors[a] = true + } + return version, authors, nil + } + return version, authors, nil +} + +const defaultVersion = "CoreDNS" diff --git a/middleware/chaos/setup_test.go b/middleware/chaos/setup_test.go new file mode 100644 index 000000000..c1741cdf6 --- /dev/null +++ b/middleware/chaos/setup_test.go @@ -0,0 +1,63 @@ +package chaos + +import ( + "fmt" + "strings" + "testing" + + "github.com/mholt/caddy" +) + +func TestSetupChaos(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedVersion string // expected version. + expectedAuthor string // expected author (string, although we get a map). + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + // positive + { + `chaos`, false, defaultVersion, "", "", + }, + { + `chaos v2`, false, "v2", "", "", + }, + { + `chaos v3 "Miek Gieben"`, false, "v3", "Miek Gieben", "", + }, + { + fmt.Sprintf(`chaos { + %s + }`, defaultVersion), false, defaultVersion, "", "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + version, authors, err := chaosParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + + if !test.shouldErr && version != test.expectedVersion { + t.Errorf("Chaos not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedVersion, version) + } + if !test.shouldErr && authors != nil { + if _, ok := authors[test.expectedAuthor]; !ok { + t.Errorf("Chaos not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, test.expectedAuthor, "Miek Gieben") + } + } + } +} diff --git a/middleware/dnssec/cache_test.go b/middleware/dnssec/cache_test.go index 0039586d5..3062f99b0 100644 --- a/middleware/dnssec/cache_test.go +++ b/middleware/dnssec/cache_test.go @@ -22,7 +22,7 @@ func TestCacheSet(t *testing.T) { m := testMsg() state := middleware.State{Req: m} k := key(m.Answer) // calculate *before* we add the sig - d := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil) + d := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil) m = d.Sign(state, "miek.nl.", time.Now().UTC()) _, ok := d.get(k) diff --git a/middleware/dnssec/dnssec.go b/middleware/dnssec/dnssec.go index 4907f1621..f517bfe2c 100644 --- a/middleware/dnssec/dnssec.go +++ b/middleware/dnssec/dnssec.go @@ -11,14 +11,15 @@ import ( ) type Dnssec struct { - Next middleware.Handler + Next middleware.Handler + zones []string keys []*DNSKEY inflight *singleflight.Group cache *gcache.Cache } -func NewDnssec(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec { +func New(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec { return Dnssec{Next: next, zones: zones, keys: keys, diff --git a/middleware/dnssec/dnssec_test.go b/middleware/dnssec/dnssec_test.go index 49b0d5d3a..10f731325 100644 --- a/middleware/dnssec/dnssec_test.go +++ b/middleware/dnssec/dnssec_test.go @@ -69,7 +69,7 @@ func TestSigningDifferentZone(t *testing.T) { m := testMsgEx() state := middleware.State{Req: m} - d := NewDnssec([]string{"example.org."}, []*DNSKEY{key}, nil) + d := New([]string{"example.org."}, []*DNSKEY{key}, nil) m = d.Sign(state, "example.org.", time.Now().UTC()) if !section(m.Answer, 1) { t.Errorf("answer section should have 1 sig") @@ -158,7 +158,7 @@ func testDelegationMsg() *dns.Msg { func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { k, rm1, rm2 := newKey(t) - d := NewDnssec(zones, []*DNSKEY{k}, nil) + d := New(zones, []*DNSKEY{k}, nil) return d, rm1, rm2 } diff --git a/middleware/dnssec/handler_test.go b/middleware/dnssec/handler_test.go index 6f537b90e..f7cb7e680 100644 --- a/middleware/dnssec/handler_test.go +++ b/middleware/dnssec/handler_test.go @@ -77,7 +77,7 @@ func TestLookupZone(t *testing.T) { dnskey, rm1, rm2 := newKey(t) defer rm1() defer rm2() - dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm) + dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm) ctx := context.TODO() for _, tc := range dnsTestCases { @@ -115,7 +115,7 @@ func TestLookupDNSKEY(t *testing.T) { dnskey, rm1, rm2 := newKey(t) defer rm1() defer rm2() - dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler()) + dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler()) ctx := context.TODO() for _, tc := range dnssecTestCases { diff --git a/middleware/dnssec/setup.go b/middleware/dnssec/setup.go new file mode 100644 index 000000000..999f85bf8 --- /dev/null +++ b/middleware/dnssec/setup.go @@ -0,0 +1,91 @@ +package dnssec + +import ( + "strings" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("dnssec", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + zones, keys, err := dnssecParse(c) + if err != nil { + return err + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return New(zones, keys, next) + }) + + return nil +} + +func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, error) { + zones := []string{} + + keys := []*DNSKEY{} + for c.Next() { + if c.Val() == "dnssec" { + // dnssec [zones...] + zones = make([]string, len(c.ServerBlockKeys)) + copy(zones, c.ServerBlockKeys) + args := c.RemainingArgs() + if len(args) > 0 { + zones = args + } + + for c.NextBlock() { + k, e := keyParse(c) + if e != nil { + return nil, nil, e + } + keys = append(keys, k...) + } + } + } + for i, _ := range zones { + zones[i] = middleware.Host(zones[i]).Normalize() + } + return zones, keys, nil +} + +func keyParse(c *caddy.Controller) ([]*DNSKEY, error) { + keys := []*DNSKEY{} + + what := c.Val() + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + switch what { + case "key": + if value == "file" { + ks := c.RemainingArgs() + for _, k := range ks { + base := k + // Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205 + if strings.HasSuffix(k, ".key") { + base = k[:len(k)-4] + } + if strings.HasSuffix(k, ".private") { + base = k[:len(k)-8] + } + k, err := ParseKeyFile(base+".key", base+".private") + if err != nil { + return nil, err + } + keys = append(keys, k) + } + } + } + return keys, nil +} diff --git a/middleware/dnssec/setup_test.go b/middleware/dnssec/setup_test.go new file mode 100644 index 000000000..9dbeb77fd --- /dev/null +++ b/middleware/dnssec/setup_test.go @@ -0,0 +1,56 @@ +package dnssec + +import ( + "strings" + "testing" + + "github.com/mholt/caddy" +) + +func TestSetupDnssec(t *testing.T) { + tests := []struct { + input string + shouldErr bool + expectedZones []string + expectedKeys []string + expectedErrContent string + }{ + { + `dnssec`, false, nil, nil, "", + }, + { + `dnssec miek.nl`, false, []string{"miek.nl."}, nil, "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + zones, keys, err := dnssecParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + if !test.shouldErr { + for i, z := range test.expectedZones { + if zones[i] != z { + t.Errorf("Dnssec not correctly set for input %s. Expected: %s, actual: %s", test.input, z, zones[i]) + } + } + for i, k := range test.expectedKeys { + if k != keys[i].K.Header().Name { + t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name) + } + } + } + } +} diff --git a/middleware/errors/setup.go b/middleware/errors/setup.go new file mode 100644 index 000000000..e1c77373d --- /dev/null +++ b/middleware/errors/setup.go @@ -0,0 +1,133 @@ +package errors + +import ( + "io" + "log" + "os" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + + "github.com/hashicorp/go-syslog" + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("errors", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + handler, err := errorsParse(c) + if err != nil { + return err + } + + var writer io.Writer + + switch handler.LogFile { + case "visible": + handler.Debug = true + case "stdout": + writer = os.Stdout + case "stderr": + writer = os.Stderr + case "syslog": + writer, err = gsyslog.NewLogger(gsyslog.LOG_ERR, "LOCAL0", "coredns") + if err != nil { + return err + } + default: + if handler.LogFile == "" { + writer = os.Stderr // default + break + } + + var file *os.File + file, err = os.OpenFile(handler.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return err + } + if handler.LogRoller != nil { + file.Close() + + handler.LogRoller.Filename = handler.LogFile + + writer = handler.LogRoller.GetLogWriter() + } else { + writer = file + } + } + handler.Log = log.New(writer, "", 0) + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + handler.Next = next + return handler + }) + + return nil +} + +func errorsParse(c *caddy.Controller) (ErrorHandler, error) { + handler := ErrorHandler{} + + optionalBlock := func() (bool, error) { + var hadBlock bool + + for c.NextBlock() { + hadBlock = true + + what := c.Val() + if !c.NextArg() { + return hadBlock, c.ArgErr() + } + where := c.Val() + + if what == "log" { + if where == "visible" { + handler.Debug = true + } else { + handler.LogFile = where + if c.NextArg() { + if c.Val() == "{" { + c.IncrNest() + logRoller, err := middleware.ParseRoller(c) + if err != nil { + return hadBlock, err + } + handler.LogRoller = logRoller + } + } + } + } + } + return hadBlock, nil + } + + for c.Next() { + // weird hack to avoid having the handler values overwritten. + if c.Val() == "}" { + continue + } + // Configuration may be in a block + hadBlock, err := optionalBlock() + if err != nil { + return handler, err + } + + // Otherwise, the only argument would be an error log file name or 'visible' + if !hadBlock { + if c.NextArg() { + if c.Val() == "visible" { + handler.Debug = true + } else { + handler.LogFile = c.Val() + } + } + } + } + + return handler, nil +} diff --git a/middleware/errors/setup_test.go b/middleware/errors/setup_test.go new file mode 100644 index 000000000..6e5a85d08 --- /dev/null +++ b/middleware/errors/setup_test.go @@ -0,0 +1,98 @@ +package errors + +import ( + "testing" + + "github.com/mholt/caddy" + "github.com/miekg/coredns/middleware" +) + +func TestErrorsParse(t *testing.T) { + tests := []struct { + inputErrorsRules string + shouldErr bool + expectedErrorHandler ErrorHandler + }{ + {`errors`, false, ErrorHandler{ + LogFile: "", + }}, + {`errors errors.txt`, false, ErrorHandler{ + LogFile: "errors.txt", + }}, + {`errors visible`, false, ErrorHandler{ + LogFile: "", + Debug: true, + }}, + {`errors { log visible }`, false, ErrorHandler{ + LogFile: "", + Debug: true, + }}, + {`errors { log errors.txt { size 2 age 10 keep 3 } }`, false, ErrorHandler{ + LogFile: "errors.txt", + LogRoller: &middleware.LogRoller{ + MaxSize: 2, + MaxAge: 10, + MaxBackups: 3, + LocalTime: true, + }, + }}, + {`errors { log errors.txt { + size 3 + age 11 + keep 5 + } +}`, false, ErrorHandler{ + LogFile: "errors.txt", + LogRoller: &middleware.LogRoller{ + MaxSize: 3, + MaxAge: 11, + MaxBackups: 5, + LocalTime: true, + }, + }}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputErrorsRules) + actualErrorsRule, err := errorsParse(c) + + if err == nil && test.shouldErr { + t.Errorf("Test %d didn't error, but it should have", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) + } + if actualErrorsRule.LogFile != test.expectedErrorHandler.LogFile { + t.Errorf("Test %d expected LogFile to be %s, but got %s", + i, test.expectedErrorHandler.LogFile, actualErrorsRule.LogFile) + } + if actualErrorsRule.Debug != test.expectedErrorHandler.Debug { + t.Errorf("Test %d expected Debug to be %v, but got %v", + i, test.expectedErrorHandler.Debug, actualErrorsRule.Debug) + } + if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller == nil || actualErrorsRule.LogRoller == nil && test.expectedErrorHandler.LogRoller != nil { + t.Fatalf("Test %d expected LogRoller to be %v, but got %v", + i, test.expectedErrorHandler.LogRoller, actualErrorsRule.LogRoller) + } + if actualErrorsRule.LogRoller != nil && test.expectedErrorHandler.LogRoller != nil { + if actualErrorsRule.LogRoller.Filename != test.expectedErrorHandler.LogRoller.Filename { + t.Fatalf("Test %d expected LogRoller Filename to be %s, but got %s", + i, test.expectedErrorHandler.LogRoller.Filename, actualErrorsRule.LogRoller.Filename) + } + if actualErrorsRule.LogRoller.MaxAge != test.expectedErrorHandler.LogRoller.MaxAge { + t.Fatalf("Test %d expected LogRoller MaxAge to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxAge, actualErrorsRule.LogRoller.MaxAge) + } + if actualErrorsRule.LogRoller.MaxBackups != test.expectedErrorHandler.LogRoller.MaxBackups { + t.Fatalf("Test %d expected LogRoller MaxBackups to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxBackups, actualErrorsRule.LogRoller.MaxBackups) + } + if actualErrorsRule.LogRoller.MaxSize != test.expectedErrorHandler.LogRoller.MaxSize { + t.Fatalf("Test %d expected LogRoller MaxSize to be %d, but got %d", + i, test.expectedErrorHandler.LogRoller.MaxSize, actualErrorsRule.LogRoller.MaxSize) + } + if actualErrorsRule.LogRoller.LocalTime != test.expectedErrorHandler.LogRoller.LocalTime { + t.Fatalf("Test %d expected LogRoller LocalTime to be %t, but got %t", + i, test.expectedErrorHandler.LogRoller.LocalTime, actualErrorsRule.LogRoller.LocalTime) + } + } + } +} diff --git a/middleware/etcd/cname_test.go b/middleware/etcd/cname_test.go index 4a00c05c2..ee341b7b6 100644 --- a/middleware/etcd/cname_test.go +++ b/middleware/etcd/cname_test.go @@ -16,6 +16,8 @@ import ( // Check the ordering of returned cname. func TestCnameLookup(t *testing.T) { + etc := newEtcdMiddleware() + for _, serv := range servicesCname { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) diff --git a/middleware/etcd/debug_test.go b/middleware/etcd/debug_test.go index 91796816f..82de9fe1f 100644 --- a/middleware/etcd/debug_test.go +++ b/middleware/etcd/debug_test.go @@ -30,12 +30,13 @@ func TestIsDebug(t *testing.T) { } func TestDebugLookup(t *testing.T) { + etc := newEtcdMiddleware() + etc.Debug = true + for _, serv := range servicesDebug { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) } - etc.Debug = true - defer func() { etc.Debug = false }() for _, tc := range dnsTestCasesDebug { m := tc.Msg() @@ -69,6 +70,8 @@ func TestDebugLookup(t *testing.T) { } func TestDebugLookupFalse(t *testing.T) { + etc := newEtcdMiddleware() + for _, serv := range servicesDebug { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) diff --git a/middleware/etcd/group_test.go b/middleware/etcd/group_test.go index f5283e3ba..7a2808d45 100644 --- a/middleware/etcd/group_test.go +++ b/middleware/etcd/group_test.go @@ -14,6 +14,8 @@ import ( ) func TestGroupLookup(t *testing.T) { + etc := newEtcdMiddleware() + for _, serv := range servicesGroup { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) diff --git a/middleware/etcd/multi_test.go b/middleware/etcd/multi_test.go index 19e6ca7a3..f4b59f50b 100644 --- a/middleware/etcd/multi_test.go +++ b/middleware/etcd/multi_test.go @@ -14,10 +14,9 @@ import ( ) func TestMultiLookup(t *testing.T) { + etc := newEtcdMiddleware() etc.Zones = []string{"skydns.test.", "miek.nl."} - defer func() { etc.Zones = []string{"skydns.test.", "skydns_extra.test.", "in-addr.arpa."} }() etc.Next = test.ErrorHandler() - defer func() { etc.Next = nil }() for _, serv := range servicesMulti { set(t, etc, serv.Key, 0, serv) diff --git a/middleware/etcd/other_test.go b/middleware/etcd/other_test.go index 34971c6f1..ff37d27d2 100644 --- a/middleware/etcd/other_test.go +++ b/middleware/etcd/other_test.go @@ -18,6 +18,8 @@ import ( ) func TestOtherLookup(t *testing.T) { + etc := newEtcdMiddleware() + for _, serv := range servicesOther { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) diff --git a/middleware/etcd/proxy_lookup_test.go b/middleware/etcd/proxy_lookup_test.go index 9a31eee24..5e0999fb0 100644 --- a/middleware/etcd/proxy_lookup_test.go +++ b/middleware/etcd/proxy_lookup_test.go @@ -15,18 +15,15 @@ import ( ) func TestProxyLookupFailDebug(t *testing.T) { + etc := newEtcdMiddleware() + etc.Proxy = proxy.New([]string{"127.0.0.1:154"}) + etc.Debug = true + for _, serv := range servicesProxy { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) } - prxy := etc.Proxy - etc.Proxy = proxy.New([]string{"127.0.0.1:154"}) - defer func() { etc.Proxy = prxy }() - - etc.Debug = true - defer func() { etc.Debug = false }() - for _, tc := range dnsTestCasesProxy { m := tc.Msg() diff --git a/middleware/etcd/setup.go b/middleware/etcd/setup.go new file mode 100644 index 000000000..dc1dddb0e --- /dev/null +++ b/middleware/etcd/setup.go @@ -0,0 +1,207 @@ +package etcd + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "net/http" + "time" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/singleflight" + + etcdc "github.com/coreos/etcd/client" + "github.com/mholt/caddy" + "golang.org/x/net/context" +) + +func init() { + caddy.RegisterPlugin("etcd", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + e, stubzones, err := etcdParse(c) + if err != nil { + return err + } + if stubzones { + c.OnStartup(func() error { + e.UpdateStubZones() + return nil + }) + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + e.Next = next + return e + }) + + return nil +} + +func etcdParse(c *caddy.Controller) (*Etcd, bool, error) { + stub := make(map[string]proxy.Proxy) + etc := Etcd{ + Proxy: proxy.New([]string{"8.8.8.8:53", "8.8.4.4:53"}), + PathPrefix: "skydns", + Ctx: context.Background(), + Inflight: &singleflight.Group{}, + Stubmap: &stub, + } + var ( + client etcdc.KeysAPI + tlsCertFile = "" + tlsKeyFile = "" + tlsCAcertFile = "" + endpoints = []string{defaultEndpoint} + stubzones = false + ) + for c.Next() { + if c.Val() == "etcd" { + etc.Client = client + etc.Zones = c.RemainingArgs() + if len(etc.Zones) == 0 { + etc.Zones = make([]string, len(c.ServerBlockKeys)) + copy(etc.Zones, c.ServerBlockKeys) + } + middleware.Zones(etc.Zones).FullyQualify() + if c.NextBlock() { + // TODO(miek): 2 switches? + switch c.Val() { + case "stubzones": + stubzones = true + case "debug": + etc.Debug = true + case "path": + if !c.NextArg() { + return &Etcd{}, false, c.ArgErr() + } + etc.PathPrefix = c.Val() + case "endpoint": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, false, c.ArgErr() + } + endpoints = args + case "upstream": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, false, c.ArgErr() + } + for i := 0; i < len(args); i++ { + h, p, e := net.SplitHostPort(args[i]) + if e != nil && p == "" { + args[i] = h + ":53" + } + } + endpoints = args + etc.Proxy = proxy.New(args) + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) != 3 { + return &Etcd{}, false, c.ArgErr() + } + tlsCertFile, tlsKeyFile, tlsCAcertFile = args[0], args[1], args[2] + } + for c.Next() { + switch c.Val() { + case "stubzones": + stubzones = true + case "debug": + etc.Debug = true + case "path": + if !c.NextArg() { + return &Etcd{}, false, c.ArgErr() + } + etc.PathPrefix = c.Val() + case "endpoint": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, false, c.ArgErr() + } + endpoints = args + case "upstream": + args := c.RemainingArgs() + if len(args) == 0 { + return &Etcd{}, false, c.ArgErr() + } + for i := 0; i < len(args); i++ { + h, p, e := net.SplitHostPort(args[i]) + if e != nil && p == "" { + args[i] = h + ":53" + } + } + etc.Proxy = proxy.New(args) + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) != 3 { + return &Etcd{}, false, c.ArgErr() + } + tlsCertFile, tlsKeyFile, tlsCAcertFile = args[0], args[1], args[2] + } + } + } + client, err := newEtcdClient(endpoints, tlsCertFile, tlsKeyFile, tlsCAcertFile) + if err != nil { + return &Etcd{}, false, err + } + etc.Client = client + return &etc, stubzones, nil + } + } + return &Etcd{}, false, nil +} + +func newEtcdClient(endpoints []string, tlsCert, tlsKey, tlsCACert string) (etcdc.KeysAPI, error) { + etcdCfg := etcdc.Config{ + Endpoints: endpoints, + Transport: newHTTPSTransport(tlsCert, tlsKey, tlsCACert), + } + cli, err := etcdc.New(etcdCfg) + if err != nil { + return nil, err + } + return etcdc.NewKeysAPI(cli), nil +} + +func newHTTPSTransport(tlsCertFile, tlsKeyFile, tlsCACertFile string) etcdc.CancelableTransport { + var cc *tls.Config = nil + + if tlsCertFile != "" && tlsKeyFile != "" { + var rpool *x509.CertPool + if tlsCACertFile != "" { + if pemBytes, err := ioutil.ReadFile(tlsCACertFile); err == nil { + rpool = x509.NewCertPool() + rpool.AppendCertsFromPEM(pemBytes) + } + } + + if tlsCert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile); err == nil { + cc = &tls.Config{ + RootCAs: rpool, + Certificates: []tls.Certificate{tlsCert}, + InsecureSkipVerify: true, + } + } + } + + tr := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cc, + } + + return tr +} + +const defaultEndpoint = "http://localhost:2379" diff --git a/middleware/etcd/setup_test.go b/middleware/etcd/setup_test.go index 799b4a1bb..b522345d2 100644 --- a/middleware/etcd/setup_test.go +++ b/middleware/etcd/setup_test.go @@ -19,20 +19,19 @@ import ( "golang.org/x/net/context" ) -var ( - etc *Etcd - client etcdc.KeysAPI - ctxt context.Context -) - func init() { ctxt, _ = context.WithTimeout(context.Background(), etcdTimeout) +} + +// etc *Etcd +func newEtcdMiddleware() *Etcd { + ctxt, _ = context.WithTimeout(context.Background(), etcdTimeout) etcdCfg := etcdc.Config{ Endpoints: []string{"http://localhost:2379"}, } cli, _ := etcdc.New(etcdCfg) - etc = &Etcd{ + return &Etcd{ Proxy: proxy.New([]string{"8.8.8.8:53"}), PathPrefix: "skydns", Ctx: context.Background(), @@ -57,10 +56,12 @@ func delete(t *testing.T, e *Etcd, k string) { } func TestLookup(t *testing.T) { + etc := newEtcdMiddleware() for _, serv := range services { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) } + for _, tc := range dnsTestCases { m := tc.Msg() @@ -91,3 +92,5 @@ func TestLookup(t *testing.T) { } } } + +var ctxt context.Context diff --git a/middleware/etcd/stub_test.go b/middleware/etcd/stub_test.go index 1dc0901c0..b5a101dad 100644 --- a/middleware/etcd/stub_test.go +++ b/middleware/etcd/stub_test.go @@ -41,13 +41,14 @@ func TestStubLookup(t *testing.T) { exampleNetStub := &msg.Service{Host: host, Port: port, Key: "a.example.net.stub.dns.skydns.test."} servicesStub = append(servicesStub, exampleNetStub) + etc := newEtcdMiddleware() + for _, serv := range servicesStub { set(t, etc, serv.Key, 0, serv) defer delete(t, etc, serv.Key) } etc.updateStubZones() - defer func() { etc.Stubmap = nil }() for _, tc := range dnsTestCasesStub { m := tc.Msg() diff --git a/middleware/file/setup.go b/middleware/file/setup.go new file mode 100644 index 000000000..8b44650ee --- /dev/null +++ b/middleware/file/setup.go @@ -0,0 +1,143 @@ +package file + +import ( + "fmt" + "net" + "os" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("file", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + zones, err := fileParse(c) + if err != nil { + return err + } + + // Add startup functions to notify the master(s). + for _, n := range zones.Names { + c.OnStartup(func() error { + zones.Z[n].StartupOnce.Do(func() { + if len(zones.Z[n].TransferTo) > 0 { + zones.Z[n].Notify() + } + zones.Z[n].Reload(nil) + }) + return nil + }) + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return File{Next: next, Zones: zones} + }) + + return nil +} + +func fileParse(c *caddy.Controller) (Zones, error) { + z := make(map[string]*Zone) + names := []string{} + origins := []string{} + + for c.Next() { + if c.Val() == "file" { + // file db.file [zones...] + if !c.NextArg() { + return Zones{}, c.ArgErr() + } + fileName := c.Val() + + origins = make([]string, len(c.ServerBlockKeys)) + copy(origins, c.ServerBlockKeys) + args := c.RemainingArgs() + if len(args) > 0 { + origins = args + } + + reader, err := os.Open(fileName) + if err != nil { + // bail out + return Zones{}, err + } + + for i, _ := range origins { + origins[i] = middleware.Host(origins[i]).Normalize() + zone, err := Parse(reader, origins[i], fileName) + if err == nil { + z[origins[i]] = zone + } else { + return Zones{}, err + } + names = append(names, origins[i]) + } + + noReload := false + for c.NextBlock() { + t, _, e := TransferParse(c) + if e != nil { + return Zones{}, e + } + switch c.Val() { + case "no_reload": + noReload = true + } + // discard from, here, maybe check and show log when we do? + for _, origin := range origins { + if t != nil { + z[origin].TransferTo = append(z[origin].TransferTo, t...) + } + z[origin].NoReload = noReload + } + } + } + } + return Zones{Z: z, Names: names}, nil +} + +// TransferParse parses transfer statements: 'transfer to [address...]'. +// Exported so secondary can use this as well. +func TransferParse(c *caddy.Controller) (tos, froms []string, err error) { + what := c.Val() + if !c.NextArg() { + return nil, nil, c.ArgErr() + } + value := c.Val() + switch what { + case "transfer": + if value == "to" { + tos = c.RemainingArgs() + for i, _ := range tos { + if tos[i] != "*" { + if x := net.ParseIP(tos[i]); x == nil { + return nil, nil, fmt.Errorf("must specify an IP addres: `%s'", tos[i]) + } + tos[i] = middleware.Addr(tos[i]).Normalize() + } + } + } + if value == "from" { + froms = c.RemainingArgs() + for i, _ := range froms { + if froms[i] != "*" { + if x := net.ParseIP(froms[i]); x == nil { + return nil, nil, fmt.Errorf("must specify an IP addres: `%s'", froms[i]) + } + froms[i] = middleware.Addr(froms[i]).Normalize() + } else { + return nil, nil, fmt.Errorf("can't use '*' in transfer from") + } + } + } + } + return +} diff --git a/middleware/health/health.go b/middleware/health/health.go index 035c9ca7a..1d47e409e 100644 --- a/middleware/health/health.go +++ b/middleware/health/health.go @@ -12,15 +12,16 @@ var once sync.Once type Health struct { Addr string - ln net.Listener - mux *http.ServeMux + + ln net.Listener + mux *http.ServeMux } func health(w http.ResponseWriter, r *http.Request) { io.WriteString(w, ok) } -func (h *Health) Start() error { +func (h *Health) Startup() error { if h.Addr == "" { h.Addr = defAddr } diff --git a/middleware/health/setup.go b/middleware/health/setup.go new file mode 100644 index 000000000..cf7667d17 --- /dev/null +++ b/middleware/health/setup.go @@ -0,0 +1,42 @@ +package health + +import "github.com/mholt/caddy" + +func init() { + caddy.RegisterPlugin("health", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + addr, err := healthParse(c) + if err != nil { + return err + } + + health := &Health{Addr: addr} + c.OnStartup(health.Startup) + c.OnShutdown(health.Shutdown) + + // Don't do AddMiddleware, as health is not *really* a middleware just a separate + // webserver running. + + return nil +} + +func healthParse(c *caddy.Controller) (string, error) { + addr := "" + for c.Next() { + args := c.RemainingArgs() + + switch len(args) { + case 0: + case 1: + addr = args[0] + default: + return "", c.ArgErr() + } + } + return addr, nil +} diff --git a/middleware/kubernetes/controller.go b/middleware/kubernetes/controller.go index 3fbea313e..5de16d61c 100644 --- a/middleware/kubernetes/controller.go +++ b/middleware/kubernetes/controller.go @@ -2,7 +2,6 @@ package kubernetes import ( "fmt" - "log" "sync" "time" @@ -12,7 +11,7 @@ import ( "k8s.io/kubernetes/pkg/client/cache" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/controller/framework" - "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/watch" ) @@ -24,7 +23,7 @@ var ( type dnsController struct { client *client.Client - selector *labels.Selector + selector *labels.Selector endpController *framework.Controller svcController *framework.Controller @@ -45,9 +44,9 @@ type dnsController struct { // newDNSController creates a controller for coredns func newdnsController(kubeClient *client.Client, resyncPeriod time.Duration, lselector *labels.Selector) *dnsController { dns := dnsController{ - client: kubeClient, - selector: lselector, - stopCh: make(chan struct{}), + client: kubeClient, + selector: lselector, + stopCh: make(chan struct{}), } dns.endpLister.Store, dns.endpController = framework.NewInformer( @@ -76,54 +75,54 @@ func newdnsController(kubeClient *client.Client, resyncPeriod time.Duration, lse func serviceListFunc(c *client.Client, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) { return func(opts api.ListOptions) (runtime.Object, error) { - if s != nil { - opts.LabelSelector = *s - } + if s != nil { + opts.LabelSelector = *s + } return c.Services(ns).List(opts) } } func serviceWatchFunc(c *client.Client, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) { return func(options api.ListOptions) (watch.Interface, error) { - if s != nil { - options.LabelSelector = *s - } + if s != nil { + options.LabelSelector = *s + } return c.Services(ns).Watch(options) } } func endpointsListFunc(c *client.Client, ns string, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) { return func(opts api.ListOptions) (runtime.Object, error) { - if s != nil { - opts.LabelSelector = *s - } + if s != nil { + opts.LabelSelector = *s + } return c.Endpoints(ns).List(opts) } } func endpointsWatchFunc(c *client.Client, ns string, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) { return func(options api.ListOptions) (watch.Interface, error) { - if s != nil { - options.LabelSelector = *s - } + if s != nil { + options.LabelSelector = *s + } return c.Endpoints(ns).Watch(options) } } func namespaceListFunc(c *client.Client, s *labels.Selector) func(api.ListOptions) (runtime.Object, error) { return func(opts api.ListOptions) (runtime.Object, error) { - if s != nil { - opts.LabelSelector = *s - } + if s != nil { + opts.LabelSelector = *s + } return c.Namespaces().List(opts) } } func namespaceWatchFunc(c *client.Client, s *labels.Selector) func(options api.ListOptions) (watch.Interface, error) { return func(options api.ListOptions) (watch.Interface, error) { - if s != nil { - options.LabelSelector = *s - } + if s != nil { + options.LabelSelector = *s + } return c.Namespaces().Watch(options) } } @@ -140,7 +139,6 @@ func (dns *dnsController) Stop() error { // Only try draining the workqueue if we haven't already. if !dns.shutdown { close(dns.stopCh) - log.Println("shutting down controller queues") dns.shutdown = true return nil @@ -151,14 +149,10 @@ func (dns *dnsController) Stop() error { // Run starts the controller. func (dns *dnsController) Run() { - log.Println("[debug] Starting k8s notification controllers") - go dns.endpController.Run(dns.stopCh) go dns.svcController.Run(dns.stopCh) go dns.nsController.Run(dns.stopCh) - <-dns.stopCh - log.Println("[debug] shutting down coredns controller") } func (dns *dnsController) GetNamespaceList() *api.NamespaceList { @@ -203,12 +197,12 @@ func (dns *dnsController) GetServiceInNamespace(namespace string, servicename st svcObj, svcExists, err := dns.svcLister.Store.GetByKey(svcKey) if err != nil { - log.Printf("error getting service %v from the cache: %v\n", svcKey, err) + // TODO(...): should return err here return nil } if !svcExists { - log.Printf("service %v does not exists\n", svcKey) + // TODO(...): should return err here return nil } diff --git a/middleware/kubernetes/handler.go b/middleware/kubernetes/handler.go index 05dfba934..1986820d5 100644 --- a/middleware/kubernetes/handler.go +++ b/middleware/kubernetes/handler.go @@ -2,7 +2,6 @@ package kubernetes import ( "fmt" - "log" "strings" "github.com/miekg/coredns/middleware" @@ -12,8 +11,6 @@ import ( ) func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - log.Printf("[debug] here entering ServeDNS: ctx:%v dnsmsg:%v\n", ctx, r) - state := middleware.State{W: w, Req: r} if state.QClass() != dns.ClassINET { return dns.RcodeServerFailure, fmt.Errorf("can only deal with ClassINET") diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 5e2a1bf53..59a044140 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -16,10 +16,10 @@ import ( "github.com/miekg/dns" "k8s.io/kubernetes/pkg/api" unversionedapi "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/labels" unversionedclient "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" + "k8s.io/kubernetes/pkg/labels" ) type Kubernetes struct { @@ -32,10 +32,10 @@ type Kubernetes struct { NameTemplate *nametemplate.NameTemplate Namespaces []string LabelSelector *unversionedapi.LabelSelector - Selector *labels.Selector + Selector *labels.Selector } -func (g *Kubernetes) StartKubeCache() error { +func (g *Kubernetes) InitKubeCache() error { // For a custom api server or running outside a k8s cluster // set URL in env.KUBERNETES_MASTER or set endpoint in Corefile loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() @@ -46,7 +46,6 @@ func (g *Kubernetes) StartKubeCache() error { clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) config, err := clientConfig.ClientConfig() if err != nil { - log.Printf("[debug] error connecting to the client: %v", err) return err } kubeClient, err := unversionedclient.New(config) @@ -58,20 +57,17 @@ func (g *Kubernetes) StartKubeCache() error { if g.LabelSelector == nil { log.Printf("[INFO] Kubernetes middleware configured without a label selector. No label-based filtering will be performed.") } else { - var selector labels.Selector + var selector labels.Selector selector, err = unversionedapi.LabelSelectorAsSelector(g.LabelSelector) - g.Selector = &selector - if err != nil { - log.Printf("[ERROR] Unable to create Selector for LabelSelector '%s'.Error was: %s", g.LabelSelector, err) - return err - } + g.Selector = &selector + if err != nil { + log.Printf("[ERROR] Unable to create Selector for LabelSelector '%s'.Error was: %s", g.LabelSelector, err) + return err + } log.Printf("[INFO] Kubernetes middleware configured with the label selector '%s'. Only kubernetes objects matching this label selector will be exposed.", unversionedapi.FormatLabelSelector(g.LabelSelector)) } - log.Printf("[debug] Starting kubernetes middleware with k8s API resync period: %s", g.ResyncPeriod) g.APIConn = newdnsController(kubeClient, g.ResyncPeriod, g.Selector) - go g.APIConn.Run() - return err } @@ -115,7 +111,6 @@ func (g *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { typeName string ) - log.Printf("[debug] enter Records('%v', '%v')\n", name, exact) zone, serviceSegments := g.getZoneForName(name) // TODO: Implementation above globbed together segments for the serviceName if @@ -137,30 +132,18 @@ func (g *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { serviceName = util.WildcardStar } - log.Printf("[debug] published namespaces: %v\n", g.Namespaces) - - log.Printf("[debug] exact: %v\n", exact) - log.Printf("[debug] zone: %v\n", zone) - log.Printf("[debug] servicename: %v\n", serviceName) - log.Printf("[debug] namespace: %v\n", namespace) - log.Printf("[debug] typeName: %v\n", typeName) - log.Printf("[debug] APIconn: %v\n", g.APIConn) - nsWildcard := util.SymbolContainsWildcard(namespace) serviceWildcard := util.SymbolContainsWildcard(serviceName) // Abort if the namespace does not contain a wildcard, and namespace is not published per CoreFile // Case where namespace contains a wildcard is handled in Get(...) method. if (!nsWildcard) && (len(g.Namespaces) > 0) && (!util.StringInSlice(namespace, g.Namespaces)) { - log.Printf("[debug] Namespace '%v' is not published by Corefile\n", namespace) return nil, nil } - log.Printf("before g.Get(namespace, nsWildcard, serviceName, serviceWildcard): %v %v %v %v", namespace, nsWildcard, serviceName, serviceWildcard) + log.Printf("[debug] before g.Get(namespace, nsWildcard, serviceName, serviceWildcard): %v %v %v %v", namespace, nsWildcard, serviceName, serviceWildcard) k8sItems, err := g.Get(namespace, nsWildcard, serviceName, serviceWildcard) - log.Printf("[debug] k8s items: %v\n", k8sItems) if err != nil { - log.Printf("[ERROR] Got error while looking up ServiceItems. Error is: %v\n", err) return nil, err } if k8sItems == nil { @@ -178,7 +161,6 @@ func (g *Kubernetes) getRecordsForServiceItems(serviceItems []api.Service, value for _, item := range serviceItems { clusterIP := item.Spec.ClusterIP - log.Printf("[debug] clusterIP: %v\n", clusterIP) // Create records by constructing record name from template... //values.Namespace = item.Metadata.Namespace @@ -188,13 +170,11 @@ func (g *Kubernetes) getRecordsForServiceItems(serviceItems []api.Service, value // Create records for each exposed port... for _, p := range item.Spec.Ports { - log.Printf("[debug] port: %v\n", p.Port) s := msg.Service{Host: clusterIP, Port: int(p.Port)} records = append(records, s) } } - log.Printf("[debug] records from getRecordsForServiceItems(): %v\n", records) return records } @@ -202,13 +182,6 @@ func (g *Kubernetes) getRecordsForServiceItems(serviceItems []api.Service, value func (g *Kubernetes) Get(namespace string, nsWildcard bool, servicename string, serviceWildcard bool) ([]api.Service, error) { serviceList := g.APIConn.GetServiceList() - /* TODO: Remove? - if err != nil { - log.Printf("[ERROR] Getting service list produced error: %v", err) - return nil, err - } - */ - var resultItems []api.Service for _, item := range serviceList.Items { @@ -216,7 +189,6 @@ func (g *Kubernetes) Get(namespace string, nsWildcard bool, servicename string, // If namespace has a wildcard, filter results against Corefile namespace list. // (Namespaces without a wildcard were filtered before the call to this function.) if nsWildcard && (len(g.Namespaces) > 0) && (!util.StringInSlice(item.Namespace, g.Namespaces)) { - log.Printf("[debug] Namespace '%v' is not published by Corefile\n", item.Namespace) continue } resultItems = append(resultItems, item) diff --git a/middleware/kubernetes/nametemplate/nametemplate.go b/middleware/kubernetes/nametemplate/nametemplate.go index 5a34ae4ad..3e1ac4bb3 100644 --- a/middleware/kubernetes/nametemplate/nametemplate.go +++ b/middleware/kubernetes/nametemplate/nametemplate.go @@ -2,7 +2,6 @@ package nametemplate import ( "errors" - "log" "strings" "github.com/miekg/coredns/middleware/kubernetes/util" @@ -87,17 +86,13 @@ func (t *NameTemplate) SetTemplate(s string) error { if !elementPositionSet { if strings.Contains(v, "{") { err = errors.New("Record name template contains the unknown symbol '" + v + "'") - log.Printf("[debug] %v\n", err) return err - } else { - log.Printf("[debug] Template string has static element '%v'\n", v) } } } if err == nil && !t.IsValid() { err = errors.New("Record name template does not pass NameTemplate validation") - log.Printf("[debug] %v\n", err) return err } diff --git a/middleware/kubernetes/setup.go b/middleware/kubernetes/setup.go new file mode 100644 index 000000000..fc3c036b8 --- /dev/null +++ b/middleware/kubernetes/setup.go @@ -0,0 +1,140 @@ +package kubernetes + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/kubernetes/nametemplate" + + "github.com/mholt/caddy" + unversionedapi "k8s.io/kubernetes/pkg/api/unversioned" +) + +func init() { + caddy.RegisterPlugin("kubernetes", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + kubernetes, err := kubernetesParse(c) + if err != nil { + return err + } + + err = kubernetes.InitKubeCache() + if err != nil { + return err + } + + // Register KubeCache start and stop functions with Caddy + c.OnStartup(func() error { + go kubernetes.APIConn.Run() + return nil + }) + + c.OnShutdown(func() error { + return kubernetes.APIConn.Stop() + }) + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + kubernetes.Next = next + return kubernetes + }) + + return nil +} + +func kubernetesParse(c *caddy.Controller) (Kubernetes, error) { + var err error + template := defaultNameTemplate + + k8s := Kubernetes{ResyncPeriod: defaultResyncPeriod} + k8s.NameTemplate = new(nametemplate.NameTemplate) + k8s.NameTemplate.SetTemplate(template) + + for c.Next() { + if c.Val() == "kubernetes" { + zones := c.RemainingArgs() + + if len(zones) == 0 { + k8s.Zones = make([]string, len(c.ServerBlockKeys)) + copy(k8s.Zones, c.ServerBlockKeys) + } + + k8s.Zones = NormalizeZoneList(zones) + middleware.Zones(k8s.Zones).FullyQualify() + + if k8s.Zones == nil || len(k8s.Zones) < 1 { + err = errors.New("Zone name must be provided for kubernetes middleware.") + return Kubernetes{}, err + } + + for c.NextBlock() { + switch c.Val() { + case "template": + args := c.RemainingArgs() + if len(args) != 0 { + template := strings.Join(args, "") + err = k8s.NameTemplate.SetTemplate(template) + if err != nil { + return Kubernetes{}, err + } + } else { + return Kubernetes{}, c.ArgErr() + } + case "namespaces": + args := c.RemainingArgs() + if len(args) != 0 { + k8s.Namespaces = append(k8s.Namespaces, args...) + } else { + return Kubernetes{}, c.ArgErr() + } + case "endpoint": + args := c.RemainingArgs() + if len(args) != 0 { + k8s.APIEndpoint = args[0] + } else { + return Kubernetes{}, c.ArgErr() + } + case "resyncperiod": + args := c.RemainingArgs() + if len(args) != 0 { + k8s.ResyncPeriod, err = time.ParseDuration(args[0]) + if err != nil { + err = errors.New(fmt.Sprintf("Unable to parse resync duration value. Value provided was '%v'. Example valid values: '15s', '5m', '1h'. Error was: %v", args[0], err)) + return Kubernetes{}, err + } + } else { + return Kubernetes{}, c.ArgErr() + } + case "labels": + args := c.RemainingArgs() + if len(args) != 0 { + labelSelectorString := strings.Join(args, " ") + k8s.LabelSelector, err = unversionedapi.ParseToLabelSelector(labelSelectorString) + if err != nil { + err = errors.New(fmt.Sprintf("Unable to parse label selector. Value provided was '%v'. Error was: %v", labelSelectorString, err)) + return Kubernetes{}, err + } + } else { + return Kubernetes{}, c.ArgErr() + } + } + } + return k8s, nil + } + } + err = errors.New("Kubernetes setup called without keyword 'kubernetes' in Corefile") + return Kubernetes{}, err +} + +const ( + defaultNameTemplate = "{service}.{namespace}.{zone}" + defaultResyncPeriod = 5 * time.Minute +) diff --git a/middleware/kubernetes/setup_test.go b/middleware/kubernetes/setup_test.go new file mode 100644 index 000000000..6e0918a3b --- /dev/null +++ b/middleware/kubernetes/setup_test.go @@ -0,0 +1,390 @@ +package kubernetes + +import ( + "strings" + "testing" + "time" + + "github.com/mholt/caddy" + unversionedapi "k8s.io/kubernetes/pkg/api/unversioned" +) + +func TestKubernetesParse(t *testing.T) { + tests := []struct { + description string // Human-facing description of test case + input string // Corefile data as string + shouldErr bool // true if test case is exected to produce an error. + expectedErrContent string // substring from the expected error. Empty for positive cases. + expectedZoneCount int // expected count of defined zones. + expectedNTValid bool // NameTemplate to be initialized and valid + expectedNSCount int // expected count of namespaces. + expectedResyncPeriod time.Duration // expected resync period value + expectedLabelSelector string // expected label selector value + }{ + // positive + { + "kubernetes keyword with one zone", + `kubernetes coredns.local`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "kubernetes keyword with multiple zones", + `kubernetes coredns.local test.local`, + false, + "", + 2, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "kubernetes keyword with zone and empty braces", + `kubernetes coredns.local { +}`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "endpoint keyword with url", + `kubernetes coredns.local { + endpoint http://localhost:9090 +}`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "template keyword with valid template", + `kubernetes coredns.local { + template {service}.{namespace}.{zone} +}`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "namespaces keyword with one namespace", + `kubernetes coredns.local { + namespaces demo +}`, + false, + "", + 1, + true, + 1, + defaultResyncPeriod, + "", + }, + { + "namespaces keyword with multiple namespaces", + `kubernetes coredns.local { + namespaces demo test +}`, + false, + "", + 1, + true, + 2, + defaultResyncPeriod, + "", + }, + { + "resync period in seconds", + `kubernetes coredns.local { + resyncperiod 30s +}`, + false, + "", + 1, + true, + 0, + 30 * time.Second, + "", + }, + { + "resync period in minutes", + `kubernetes coredns.local { + resyncperiod 15m +}`, + false, + "", + 1, + true, + 0, + 15 * time.Minute, + "", + }, + { + "basic label selector", + `kubernetes coredns.local { + labels environment=prod +}`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "environment=prod", + }, + { + "multi-label selector", + `kubernetes coredns.local { + labels environment in (production, staging, qa),application=nginx +}`, + false, + "", + 1, + true, + 0, + defaultResyncPeriod, + "application=nginx,environment in (production,qa,staging)", + }, + { + "fully specified valid config", + `kubernetes coredns.local test.local { + resyncperiod 15m + endpoint http://localhost:8080 + template {service}.{namespace}.{zone} + namespaces demo test + labels environment in (production, staging, qa),application=nginx +}`, + false, + "", + 2, + true, + 2, + 15 * time.Minute, + "application=nginx,environment in (production,qa,staging)", + }, + // negative + { + "no kubernetes keyword", + "", + true, + "Kubernetes setup called without keyword 'kubernetes' in Corefile", + -1, + false, + -1, + defaultResyncPeriod, + "", + }, + { + "kubernetes keyword without a zone", + `kubernetes`, + true, + "Zone name must be provided for kubernetes middleware", + -1, + true, + 0, + defaultResyncPeriod, + "", + }, + { + "endpoint keyword without an endpoint value", + `kubernetes coredns.local { + endpoint +}`, + true, + "Wrong argument count or unexpected line ending after 'endpoint'", + -1, + true, + -1, + defaultResyncPeriod, + "", + }, + { + "template keyword without a template value", + `kubernetes coredns.local { + template +}`, + true, + "Wrong argument count or unexpected line ending after 'template'", + -1, + false, + 0, + defaultResyncPeriod, + "", + }, + { + "template keyword with an invalid template value", + `kubernetes coredns.local { + template {namespace}.{zone} +}`, + true, + "Record name template does not pass NameTemplate validation", + -1, + false, + 0, + defaultResyncPeriod, + "", + }, + { + "namespace keyword without a namespace value", + `kubernetes coredns.local { + namespaces +}`, + true, + "Parse error: Wrong argument count or unexpected line ending after 'namespaces'", + -1, + true, + -1, + defaultResyncPeriod, + "", + }, + { + "resyncperiod keyword without a duration value", + `kubernetes coredns.local { + resyncperiod +}`, + true, + "Wrong argument count or unexpected line ending after 'resyncperiod'", + -1, + true, + 0, + 0 * time.Minute, + "", + }, + { + "resync period no units", + `kubernetes coredns.local { + resyncperiod 15 +}`, + true, + "Unable to parse resync duration value. Value provided was ", + -1, + true, + 0, + 0 * time.Second, + "", + }, + { + "resync period invalid", + `kubernetes coredns.local { + resyncperiod abc +}`, + true, + "Unable to parse resync duration value. Value provided was ", + -1, + true, + 0, + 0 * time.Second, + "", + }, + { + "labels with no selector value", + `kubernetes coredns.local { + labels +}`, + true, + "Wrong argument count or unexpected line ending after 'labels'", + -1, + true, + 0, + 0 * time.Second, + "", + }, + { + "labels with invalid selector value", + `kubernetes coredns.local { + labels environment in (production, qa +}`, + true, + "Unable to parse label selector. Value provided was", + -1, + true, + 0, + 0 * time.Second, + "", + }, + } + + t.Logf("Parser test cases count: %v", len(tests)) + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, err := kubernetesParse(c) + t.Logf("setup test: %2v -- %v\n", i, test.description) + //t.Logf("controller: %v\n", k8sController) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error, but did not find error for input '%s'. Error was: '%v'", i, test.input, err) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + continue + } + + if test.shouldErr && (len(test.expectedErrContent) < 1) { + t.Fatalf("Test %d: Test marked as expecting an error, but no expectedErrContent provided for input '%s'. Error was: '%v'", i, test.input, err) + } + + if test.shouldErr && (test.expectedZoneCount >= 0) { + t.Errorf("Test %d: Test marked as expecting an error, but provides value for expectedZoneCount!=-1 for input '%s'. Error was: '%v'", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + continue + } + + // No error was raised, so validate initialization of k8sController + // Zones + foundZoneCount := len(k8sController.Zones) + if foundZoneCount != test.expectedZoneCount { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d zones, instead found %d zones: '%v' for input '%s'", i, test.expectedZoneCount, foundZoneCount, k8sController.Zones, test.input) + } + + // NameTemplate + if k8sController.NameTemplate == nil { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with a NameTemplate. Instead found '%v' for input '%s'", i, k8sController.NameTemplate, test.input) + } else { + foundNTValid := k8sController.NameTemplate.IsValid() + if foundNTValid != test.expectedNTValid { + t.Errorf("Test %d: Expected NameTemplate validity to be '%v', instead found '%v' for input '%s'", i, test.expectedNTValid, foundNTValid, test.input) + } + } + + // Namespaces + foundNSCount := len(k8sController.Namespaces) + if foundNSCount != test.expectedNSCount { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with %d namespaces. Instead found %d namespaces: '%v' for input '%s'", i, test.expectedNSCount, foundNSCount, k8sController.Namespaces, test.input) + } + + // ResyncPeriod + foundResyncPeriod := k8sController.ResyncPeriod + if foundResyncPeriod != test.expectedResyncPeriod { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with resync period '%s'. Instead found period '%s' for input '%s'", i, test.expectedResyncPeriod, foundResyncPeriod, test.input) + } + + // Labels + if k8sController.LabelSelector != nil { + foundLabelSelectorString := unversionedapi.FormatLabelSelector(k8sController.LabelSelector) + if foundLabelSelectorString != test.expectedLabelSelector { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with label selector '%s'. Instead found selector '%s' for input '%s'", i, test.expectedLabelSelector, foundLabelSelectorString, test.input) + } + } + } +} diff --git a/middleware/loadbalance/setup.go b/middleware/loadbalance/setup.go new file mode 100644 index 000000000..ef3f35f03 --- /dev/null +++ b/middleware/loadbalance/setup.go @@ -0,0 +1,25 @@ +package loadbalance + +import ( + "github.com/mholt/caddy" + "github.com/miekg/coredns/core/dnsserver" +) + +func init() { + caddy.RegisterPlugin("loadbalance", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + for c.Next() { + // TODO(miek): block and option parsing + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return RoundRobin{Next: next} + }) + + return nil +} diff --git a/middleware/log/setup.go b/middleware/log/setup.go new file mode 100644 index 000000000..a80e69ac3 --- /dev/null +++ b/middleware/log/setup.go @@ -0,0 +1,141 @@ +package log + +import ( + "io" + "log" + "os" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/server" + + "github.com/hashicorp/go-syslog" + "github.com/mholt/caddy" + "github.com/miekg/dns" +) + +func init() { + caddy.RegisterPlugin("log", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + rules, err := logParse(c) + if err != nil { + return err + } + + // Open the log files for writing when the server starts + c.OnStartup(func() error { + for i := 0; i < len(rules); i++ { + var err error + var writer io.Writer + + if rules[i].OutputFile == "stdout" { + writer = os.Stdout + } else if rules[i].OutputFile == "stderr" { + writer = os.Stderr + } else if rules[i].OutputFile == "syslog" { + writer, err = gsyslog.NewLogger(gsyslog.LOG_INFO, "LOCAL0", "coredns") + if err != nil { + return err + } + } else { + var file *os.File + file, err = os.OpenFile(rules[i].OutputFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + return err + } + if rules[i].Roller != nil { + file.Close() + rules[i].Roller.Filename = rules[i].OutputFile + writer = rules[i].Roller.GetLogWriter() + } else { + writer = file + } + } + + rules[i].Log = log.New(writer, "", 0) + } + + return nil + }) + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return Logger{Next: next, Rules: rules, ErrorFunc: server.DefaultErrorFunc} + }) + + return nil +} + +func logParse(c *caddy.Controller) ([]Rule, error) { + var rules []Rule + + for c.Next() { + args := c.RemainingArgs() + + var logRoller *middleware.LogRoller + if c.NextBlock() { + if c.Val() == "rotate" { + if c.NextArg() { + if c.Val() == "{" { + var err error + logRoller, err = middleware.ParseRoller(c) + if err != nil { + return nil, err + } + // This part doesn't allow having something after the rotate block + if c.Next() { + if c.Val() != "}" { + return nil, c.ArgErr() + } + } + } + } + } + } + if len(args) == 0 { + // Nothing specified; use defaults + rules = append(rules, Rule{ + NameScope: ".", + OutputFile: DefaultLogFilename, + Format: DefaultLogFormat, + Roller: logRoller, + }) + } else if len(args) == 1 { + // Only an output file specified + rules = append(rules, Rule{ + NameScope: ".", + OutputFile: args[0], + Format: DefaultLogFormat, + Roller: logRoller, + }) + } else { + // Name scope, output file, and maybe a format specified + + format := DefaultLogFormat + + if len(args) > 2 { + switch args[2] { + case "{common}": + format = CommonLogFormat + case "{combined}": + format = CombinedLogFormat + default: + format = args[2] + } + } + + rules = append(rules, Rule{ + NameScope: dns.Fqdn(args[0]), + OutputFile: args[1], + Format: format, + Roller: logRoller, + }) + } + } + + return rules, nil +} diff --git a/middleware/log/setup_test.go b/middleware/log/setup_test.go new file mode 100644 index 000000000..0a3ee63fe --- /dev/null +++ b/middleware/log/setup_test.go @@ -0,0 +1,137 @@ +package log + +import ( + "testing" + + "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy" +) + +func TestLogParse(t *testing.T) { + tests := []struct { + inputLogRules string + shouldErr bool + expectedLogRules []Rule + }{ + {`log`, false, []Rule{{ + NameScope: ".", + OutputFile: DefaultLogFilename, + Format: DefaultLogFormat, + }}}, + {`log log.txt`, false, []Rule{{ + NameScope: ".", + OutputFile: "log.txt", + Format: DefaultLogFormat, + }}}, + {`log example.org log.txt`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "log.txt", + Format: DefaultLogFormat, + }}}, + {`log example.org. stdout`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "stdout", + Format: DefaultLogFormat, + }}}, + {`log example.org log.txt {common}`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "log.txt", + Format: CommonLogFormat, + }}}, + {`log example.org accesslog.txt {combined}`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "accesslog.txt", + Format: CombinedLogFormat, + }}}, + {`log example.org. log.txt + log example.net accesslog.txt {combined}`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "log.txt", + Format: DefaultLogFormat, + }, { + NameScope: "example.net.", + OutputFile: "accesslog.txt", + Format: CombinedLogFormat, + }}}, + {`log example.org stdout {host} + log example.org log.txt {when}`, false, []Rule{{ + NameScope: "example.org.", + OutputFile: "stdout", + Format: "{host}", + }, { + NameScope: "example.org.", + OutputFile: "log.txt", + Format: "{when}", + }}}, + {`log access.log { rotate { size 2 age 10 keep 3 } }`, false, []Rule{{ + NameScope: ".", + OutputFile: "access.log", + Format: DefaultLogFormat, + Roller: &middleware.LogRoller{ + MaxSize: 2, + MaxAge: 10, + MaxBackups: 3, + LocalTime: true, + }, + }}}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.inputLogRules) + actualLogRules, err := logParse(c) + + if err == nil && test.shouldErr { + t.Errorf("Test %d didn't error, but it should have", i) + } else if err != nil && !test.shouldErr { + t.Errorf("Test %d errored, but it shouldn't have; got '%v'", i, err) + } + if len(actualLogRules) != len(test.expectedLogRules) { + t.Fatalf("Test %d expected %d no of Log rules, but got %d ", + i, len(test.expectedLogRules), len(actualLogRules)) + } + for j, actualLogRule := range actualLogRules { + + if actualLogRule.NameScope != test.expectedLogRules[j].NameScope { + t.Errorf("Test %d expected %dth LogRule NameScope to be %s , but got %s", + i, j, test.expectedLogRules[j].NameScope, actualLogRule.NameScope) + } + + if actualLogRule.OutputFile != test.expectedLogRules[j].OutputFile { + t.Errorf("Test %d expected %dth LogRule OutputFile to be %s , but got %s", + i, j, test.expectedLogRules[j].OutputFile, actualLogRule.OutputFile) + } + + if actualLogRule.Format != test.expectedLogRules[j].Format { + t.Errorf("Test %d expected %dth LogRule Format to be %s , but got %s", + i, j, test.expectedLogRules[j].Format, actualLogRule.Format) + } + if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller == nil || actualLogRule.Roller == nil && test.expectedLogRules[j].Roller != nil { + t.Fatalf("Test %d expected %dth LogRule Roller to be %v, but got %v", + i, j, test.expectedLogRules[j].Roller, actualLogRule.Roller) + } + if actualLogRule.Roller != nil && test.expectedLogRules[j].Roller != nil { + if actualLogRule.Roller.Filename != test.expectedLogRules[j].Roller.Filename { + t.Fatalf("Test %d expected %dth LogRule Roller Filename to be %s, but got %s", + i, j, test.expectedLogRules[j].Roller.Filename, actualLogRule.Roller.Filename) + } + if actualLogRule.Roller.MaxAge != test.expectedLogRules[j].Roller.MaxAge { + t.Fatalf("Test %d expected %dth LogRule Roller MaxAge to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxAge, actualLogRule.Roller.MaxAge) + } + if actualLogRule.Roller.MaxBackups != test.expectedLogRules[j].Roller.MaxBackups { + t.Fatalf("Test %d expected %dth LogRule Roller MaxBackups to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxBackups, actualLogRule.Roller.MaxBackups) + } + if actualLogRule.Roller.MaxSize != test.expectedLogRules[j].Roller.MaxSize { + t.Fatalf("Test %d expected %dth LogRule Roller MaxSize to be %d, but got %d", + i, j, test.expectedLogRules[j].Roller.MaxSize, actualLogRule.Roller.MaxSize) + } + if actualLogRule.Roller.LocalTime != test.expectedLogRules[j].Roller.LocalTime { + t.Fatalf("Test %d expected %dth LogRule Roller LocalTime to be %t, but got %t", + i, j, test.expectedLogRules[j].Roller.LocalTime, actualLogRule.Roller.LocalTime) + } + } + } + } + +} diff --git a/middleware/metrics/metrics.go b/middleware/metrics/metrics.go index 1c7db29d2..3a72bc3bb 100644 --- a/middleware/metrics/metrics.go +++ b/middleware/metrics/metrics.go @@ -34,7 +34,7 @@ type Metrics struct { ZoneNames []string } -func (m *Metrics) Start() error { +func (m *Metrics) Startup() error { m.Once.Do(func() { define() diff --git a/middleware/metrics/setup.go b/middleware/metrics/setup.go new file mode 100644 index 000000000..f31cbd4d5 --- /dev/null +++ b/middleware/metrics/setup.go @@ -0,0 +1,84 @@ +package metrics + +import ( + "sync" + + "github.com/miekg/coredns/core/dnsserver" + "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("prometheus", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + m, err := prometheusParse(c) + if err != nil { + return err + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + m.Next = next + return m + }) + + metricsOnce.Do(func() { + c.OnStartup(m.Startup) + c.OnShutdown(m.Shutdown) + }) + + return nil +} + +func prometheusParse(c *caddy.Controller) (Metrics, error) { + var ( + met Metrics + err error + ) + + for c.Next() { + if len(met.ZoneNames) > 0 { + return Metrics{}, c.Err("metrics: can only have one metrics module per server") + } + met.ZoneNames = make([]string, len(c.ServerBlockKeys)) + copy(met.ZoneNames, c.ServerBlockKeys) + for i, _ := range met.ZoneNames { + met.ZoneNames[i] = middleware.Host(met.ZoneNames[i]).Normalize() + } + args := c.RemainingArgs() + + switch len(args) { + case 0: + case 1: + met.Addr = args[0] + default: + return Metrics{}, c.ArgErr() + } + for c.NextBlock() { + switch c.Val() { + case "address": + args = c.RemainingArgs() + if len(args) != 1 { + return Metrics{}, c.ArgErr() + } + met.Addr = args[0] + default: + return Metrics{}, c.Errf("metrics: unknown item: %s", c.Val()) + } + + } + } + if met.Addr == "" { + met.Addr = addr + } + return met, err +} + +var metricsOnce sync.Once + +const addr = "localhost:9153" diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index 42102d198..f538b3091 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -12,7 +12,7 @@ type Handler struct { mux *http.ServeMux } -func (h *Handler) Start() error { +func (h *Handler) Startup() error { if ln, err := net.Listen("tcp", addr); err != nil { log.Printf("[ERROR] Failed to start pprof handler: %s", err) return err diff --git a/middleware/pprof/setup.go b/middleware/pprof/setup.go new file mode 100644 index 000000000..2b6701f35 --- /dev/null +++ b/middleware/pprof/setup.go @@ -0,0 +1,40 @@ +package pprof + +import ( + "sync" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("pprof", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + found := false + for c.Next() { + if found { + return c.Err("pprof can only be specified once") + } + if len(c.RemainingArgs()) != 0 { + return c.ArgErr() + } + if c.NextBlock() { + return c.ArgErr() + } + found = true + } + + handler := &Handler{} + pprofOnce.Do(func() { + c.OnStartup(handler.Startup) + c.OnShutdown(handler.Shutdown) + }) + + return nil +} + +var pprofOnce sync.Once diff --git a/middleware/pprof/setup_test.go b/middleware/pprof/setup_test.go new file mode 100644 index 000000000..af46fd415 --- /dev/null +++ b/middleware/pprof/setup_test.go @@ -0,0 +1,32 @@ +package pprof + +import ( + "testing" + + "github.com/mholt/caddy" +) + +func TestPProf(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + {`pprof`, false}, + {`pprof {}`, true}, + {`pprof /foo`, true}, + {`pprof { + a b + }`, true}, + {`pprof + pprof`, true}, + } + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + err := setup(c) + if test.shouldErr && err == nil { + t.Errorf("Test %v: Expected error but found nil", i) + } else if !test.shouldErr && err != nil { + t.Errorf("Test %v: Expected no error but found error: %v", i, err) + } + } +} diff --git a/middleware/proxy/setup.go b/middleware/proxy/setup.go new file mode 100644 index 000000000..81dc7777c --- /dev/null +++ b/middleware/proxy/setup.go @@ -0,0 +1,26 @@ +package proxy + +import ( + "github.com/miekg/coredns/core/dnsserver" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("proxy", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + upstreams, err := NewStaticUpstreams(c.Dispenser) + if err != nil { + return err + } + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return Proxy{Next: next, Client: Clients(), Upstreams: upstreams} + }) + + return nil +} diff --git a/middleware/proxy/upstream.go b/middleware/proxy/upstream.go index 76c27a45a..12ec00d76 100644 --- a/middleware/proxy/upstream.go +++ b/middleware/proxy/upstream.go @@ -11,8 +11,9 @@ import ( "sync/atomic" "time" - "github.com/miekg/coredns/core/parse" "github.com/miekg/coredns/middleware" + + "github.com/mholt/caddy/caddyfile" "github.com/miekg/dns" ) @@ -43,7 +44,7 @@ type Options struct { // NewStaticUpstreams parses the configuration input and sets up // static upstreams for the proxy middleware. -func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) { +func NewStaticUpstreams(c caddyfile.Dispenser) ([]Upstream, error) { var upstreams []Upstream for c.Next() { upstream := &staticUpstream{ @@ -73,7 +74,7 @@ func NewStaticUpstreams(c parse.Dispenser) ([]Upstream, error) { } for c.NextBlock() { - if err := parseBlock(&c, upstream); err != nil { + if err := parseBlock(c, upstream); err != nil { return upstreams, err } } @@ -125,7 +126,7 @@ func (u *staticUpstream) Options() Options { return u.options } -func parseBlock(c *parse.Dispenser, u *staticUpstream) error { +func parseBlock(c caddyfile.Dispenser, u *staticUpstream) error { switch c.Val() { case "policy": if !c.NextArg() { diff --git a/middleware/rewrite/setup.go b/middleware/rewrite/setup.go new file mode 100644 index 000000000..abfc0fbd6 --- /dev/null +++ b/middleware/rewrite/setup.go @@ -0,0 +1,115 @@ +package rewrite + +import ( + "strconv" + "strings" + + "github.com/miekg/coredns/core/dnsserver" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("rewrite", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + rewrites, err := rewriteParse(c) + if err != nil { + return err + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return Rewrite{Next: next, Rules: rewrites} + }) + + return nil +} + +func rewriteParse(c *caddy.Controller) ([]Rule, error) { + var simpleRules []Rule + var regexpRules []Rule + + for c.Next() { + var rule Rule + var err error + var base = "." + var pattern, to string + var status int + var ext []string + + args := c.RemainingArgs() + + var ifs []If + + switch len(args) { + case 1: + base = args[0] + fallthrough + case 0: + for c.NextBlock() { + switch c.Val() { + case "r", "regexp": + if !c.NextArg() { + return nil, c.ArgErr() + } + pattern = c.Val() + case "to": + args1 := c.RemainingArgs() + if len(args1) == 0 { + return nil, c.ArgErr() + } + to = strings.Join(args1, " ") + case "ext": // TODO(miek): fix or remove + args1 := c.RemainingArgs() + if len(args1) == 0 { + return nil, c.ArgErr() + } + ext = args1 + case "if": + args1 := c.RemainingArgs() + if len(args1) != 3 { + return nil, c.ArgErr() + } + ifCond, err := NewIf(args1[0], args1[1], args1[2]) + if err != nil { + return nil, err + } + ifs = append(ifs, ifCond) + case "status": // TODO(miek): fix or remove + if !c.NextArg() { + return nil, c.ArgErr() + } + status, _ = strconv.Atoi(c.Val()) + if status < 200 || (status > 299 && status < 400) || status > 499 { + return nil, c.Err("status must be 2xx or 4xx") + } + default: + return nil, c.ArgErr() + } + } + // ensure to or status is specified + if to == "" && status == 0 { + return nil, c.ArgErr() + } + // TODO(miek): complex rules + base, pattern, to, status, ext, ifs = base, pattern, to, status, ext, ifs + err = err + // if rule, err = NewComplexRule(base, pattern, to, status, ext, ifs); err != nil { + // return nil, err + // } + regexpRules = append(regexpRules, rule) + + // the only unhandled case is 2 and above + default: + rule = NewSimpleRule(args[0], strings.Join(args[1:], " ")) + simpleRules = append(simpleRules, rule) + } + } + + // put simple rules in front to avoid regexp computation for them + return append(simpleRules, regexpRules...), nil +} diff --git a/middleware/roller.go b/middleware/roller.go index 995cabf91..81ff71c44 100644 --- a/middleware/roller.go +++ b/middleware/roller.go @@ -2,10 +2,45 @@ package middleware import ( "io" + "strconv" + "github.com/mholt/caddy" "gopkg.in/natefinch/lumberjack.v2" ) +func ParseRoller(c *caddy.Controller) (*LogRoller, error) { + var size, age, keep int + // This is kind of a hack to support nested blocks: + // As we are already in a block: either log or errors, + // c.nesting > 0 but, as soon as c meets a }, it thinks + // the block is over and return false for c.NextBlock. + for c.NextBlock() { + what := c.Val() + if !c.NextArg() { + return nil, c.ArgErr() + } + value := c.Val() + var err error + switch what { + case "size": + size, err = strconv.Atoi(value) + case "age": + age, err = strconv.Atoi(value) + case "keep": + keep, err = strconv.Atoi(value) + } + if err != nil { + return nil, err + } + } + return &LogRoller{ + MaxSize: size, + MaxAge: age, + MaxBackups: keep, + LocalTime: true, + }, nil +} + // LogRoller implements a middleware that provides a rolling logger. type LogRoller struct { Filename string diff --git a/middleware/secondary/setup.go b/middleware/secondary/setup.go new file mode 100644 index 000000000..5550cf11d --- /dev/null +++ b/middleware/secondary/setup.go @@ -0,0 +1,82 @@ +package secondary + +import ( + "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("secondary", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + zones, err := secondaryParse(c) + if err != nil { + return err + } + + // Add startup functions to retrieve the zone and keep it up to date. + for _, n := range zones.Names { + if len(zones.Z[n].TransferFrom) > 0 { + c.OnStartup(func() error { + zones.Z[n].StartupOnce.Do(func() { + zones.Z[n].TransferIn() + go func() { + zones.Z[n].Update() + }() + }) + return nil + }) + } + } + + dnsserver.GetConfig(c).AddMiddleware(func(next dnsserver.Handler) dnsserver.Handler { + return Secondary{file.File{Next: next, Zones: zones}} + }) + + return nil +} + +func secondaryParse(c *caddy.Controller) (file.Zones, error) { + z := make(map[string]*file.Zone) + names := []string{} + origins := []string{} + for c.Next() { + if c.Val() == "secondary" { + // secondary [origin] + origins = make([]string, len(c.ServerBlockKeys)) + copy(origins, c.ServerBlockKeys) + args := c.RemainingArgs() + if len(args) > 0 { + origins = args + } + for i, _ := range origins { + origins[i] = middleware.Host(origins[i]).Normalize() + z[origins[i]] = file.NewZone(origins[i], "stdin") + names = append(names, origins[i]) + } + + for c.NextBlock() { + t, f, e := file.TransferParse(c) + if e != nil { + return file.Zones{}, e + } + for _, origin := range origins { + if t != nil { + z[origin].TransferTo = append(z[origin].TransferTo, t...) + } + if f != nil { + z[origin].TransferFrom = append(z[origin].TransferFrom, f...) + } + } + } + } + } + return file.Zones{Z: z, Names: names}, nil +} |