From 4049ed4f4b85f9f8214a03fc1bb48c579115f9e5 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Sun, 27 Aug 2017 01:32:46 +0100 Subject: [PATCH] mw/kubernetes: add configurable TTL (#995) * mw/kubernetes: add configurable TTL Add ttl option to kubernetes. This defaults to 5s but allows configuration to go up to 3600. Configure the tests so that a few actually check for the 5s, while the rest use the TTL of 303 which is ignored by the checking code. Fixes #935 * fix tests * and more --- middleware/kubernetes/README.md | 3 ++ middleware/kubernetes/handler_test.go | 32 +++++++++--------- middleware/kubernetes/kubernetes.go | 10 ++++-- middleware/kubernetes/setup.go | 14 ++++++++ middleware/kubernetes/setup_ttl_test.go | 45 +++++++++++++++++++++++++ test/kubernetes_test.go | 13 +++---- 6 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 middleware/kubernetes/setup_ttl_test.go diff --git a/middleware/kubernetes/README.md b/middleware/kubernetes/README.md index d3faeef7f..7e7cb679e 100644 --- a/middleware/kubernetes/README.md +++ b/middleware/kubernetes/README.md @@ -28,6 +28,7 @@ kubernetes [ZONES...] { labels EXPRESSION pods POD-MODE upstream ADDRESS... + ttl TTL fallthrough } ``` @@ -62,6 +63,8 @@ kubernetes [ZONES...] { * `upstream` **ADDRESS [ADDRESS...]** defines the upstream resolvers used for resolving services that point to external hosts (External Services). **ADDRESS** can be an ip, an ip:port, or a path to a file structured like resolv.conf. +* `ttl` allows you to set a custom TTL for responses. The default (and allowed minimum) is to use + 5 seconds, the maximum is capped at 3600 seconds. * `fallthrough` If a query for a record in the cluster zone results in NXDOMAIN, normally that is what the response will be. However, if you specify this option, the query will instead be passed on down the middleware chain, which can include another middleware to handle the query. diff --git a/middleware/kubernetes/handler_test.go b/middleware/kubernetes/handler_test.go index e3ddfb071..c65fd516f 100644 --- a/middleware/kubernetes/handler_test.go +++ b/middleware/kubernetes/handler_test.go @@ -16,33 +16,33 @@ var dnsTestCases = map[string](test.Case){ Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), + test.A("svc1.testns.svc.cluster.local. 5 IN A 10.0.0.1"), }, }, "A Service (wildcard)": { Qname: "svc1.*.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("svc1.*.svc.cluster.local. 0 IN A 10.0.0.1"), + test.A("svc1.*.svc.cluster.local. 5 IN A 10.0.0.1"), }, }, "SRV Service (wildcard)": { Qname: "svc1.*.svc.cluster.local.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, - Answer: []dns.RR{test.SRV("svc1.*.svc.cluster.local. 0 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")}, - Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1")}, + Answer: []dns.RR{test.SRV("svc1.*.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")}, + Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1")}, }, "SRV Service (wildcards)": { Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, - Answer: []dns.RR{test.SRV("*.any.svc1.*.svc.cluster.local. 0 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")}, - Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1")}, + Answer: []dns.RR{test.SRV("*.any.svc1.*.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local.")}, + Extra: []dns.RR{test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1")}, }, "A Service (wildcards)": { Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("*.any.svc1.*.svc.cluster.local. 0 IN A 10.0.0.1"), + test.A("*.any.svc1.*.svc.cluster.local. 303 IN A 10.0.0.1"), }, }, "SRV Service Not udp/tcp": { @@ -56,37 +56,37 @@ var dnsTestCases = map[string](test.Case){ Qname: "_http._tcp.svc1.testns.svc.cluster.local.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.SRV("_http._tcp.svc1.testns.svc.cluster.local. 0 IN SRV 0 100 80 svc1.testns.svc.cluster.local."), + test.SRV("_http._tcp.svc1.testns.svc.cluster.local. 303 IN SRV 0 100 80 svc1.testns.svc.cluster.local."), }, Extra: []dns.RR{ - test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), + test.A("svc1.testns.svc.cluster.local. 303 IN A 10.0.0.1"), }, }, "A Service (Headless)": { Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("hdls1.testns.svc.cluster.local. 0 IN A 172.0.0.2"), - test.A("hdls1.testns.svc.cluster.local. 0 IN A 172.0.0.3"), + test.A("hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.2"), + test.A("hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.3"), }, }, "SRV Service (Headless)": { Qname: "_http._tcp.hdls1.testns.svc.cluster.local.", Qtype: dns.TypeSRV, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 0 IN SRV 0 50 80 172-0-0-2.hdls1.testns.svc.cluster.local."), - test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 0 IN SRV 0 50 80 172-0-0-3.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 303 IN SRV 0 50 80 172-0-0-2.hdls1.testns.svc.cluster.local."), + test.SRV("_http._tcp.hdls1.testns.svc.cluster.local. 303 IN SRV 0 50 80 172-0-0-3.hdls1.testns.svc.cluster.local."), }, Extra: []dns.RR{ - test.A("172-0-0-2.hdls1.testns.svc.cluster.local. 0 IN A 172.0.0.2"), - test.A("172-0-0-3.hdls1.testns.svc.cluster.local. 0 IN A 172.0.0.3"), + test.A("172-0-0-2.hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.2"), + test.A("172-0-0-3.hdls1.testns.svc.cluster.local. 303 IN A 172.0.0.3"), }, }, "CNAME External": { Qname: "external.testns.svc.cluster.local.", Qtype: dns.TypeCNAME, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.CNAME("external.testns.svc.cluster.local. 0 IN CNAME ext.interwebs.test."), + test.CNAME("external.testns.svc.cluster.local. 303 IN CNAME ext.interwebs.test."), }, }, "AAAA Service (existing service)": { diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 4faa818ec..42c68e580 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -40,6 +40,7 @@ type Kubernetes struct { Namespaces map[string]bool podMode string Fallthrough bool + ttl uint32 primaryZoneIndex int interfaceAddrsFunc func() net.IP @@ -55,6 +56,7 @@ func New(zones []string) *Kubernetes { k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("127.0.0.1") } k.podMode = podModeDisabled k.Proxy = proxy.Proxy{} + k.ttl = defaultTTL return k } @@ -382,7 +384,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) { continue } - s := msg.Service{Host: addr.IP, Port: int(p.Port)} + s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl} s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name, endpointHostname(addr)}, "/") err = nil @@ -397,7 +399,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. // External service if svc.Spec.ExternalName != "" { - s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.Spec.ExternalName} + s := msg.Service{Key: strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/"), Host: svc.Spec.ExternalName, TTL: k.ttl} if t, _ := s.HostType(); t == dns.TypeCNAME { s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/") services = append(services, s) @@ -416,7 +418,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. err = nil - s := msg.Service{Host: svc.Spec.ClusterIP, Port: int(p.Port)} + s := msg.Service{Host: svc.Spec.ClusterIP, Port: int(p.Port), TTL: k.ttl} s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name}, "/") services = append(services, s) @@ -455,4 +457,6 @@ const ( Svc = "svc" // Pod is the DNS schema for kubernetes pods Pod = "pod" + // defaultTTL to apply to all answers. + defaultTTL = 5 ) diff --git a/middleware/kubernetes/setup.go b/middleware/kubernetes/setup.go index 557eca93d..15b87c2cd 100644 --- a/middleware/kubernetes/setup.go +++ b/middleware/kubernetes/setup.go @@ -3,6 +3,7 @@ package kubernetes import ( "errors" "fmt" + "strconv" "strings" "time" @@ -174,6 +175,19 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, dnsControlOpts, error) { return nil, opts, err } k8s.Proxy = proxy.NewLookup(ups) + case "ttl": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, opts, c.ArgErr() + } + t, err := strconv.Atoi(args[0]) + if err != nil { + return nil, opts, err + } + if t < 5 || t > 3600 { + return nil, opts, c.Errf("ttl must be in range [5, 3600]: %d", t) + } + k8s.ttl = uint32(t) default: return nil, opts, c.Errf("unknown property '%s'", c.Val()) } diff --git a/middleware/kubernetes/setup_ttl_test.go b/middleware/kubernetes/setup_ttl_test.go new file mode 100644 index 000000000..d58f91576 --- /dev/null +++ b/middleware/kubernetes/setup_ttl_test.go @@ -0,0 +1,45 @@ +package kubernetes + +import ( + "testing" + + "github.com/mholt/caddy" +) + +func TestKubernetesParseTTL(t *testing.T) { + tests := []struct { + input string // Corefile data as string + expectedTTL uint32 // expected count of defined zones. + shouldErr bool + }{ + {`kubernetes cluster.local { + ttl 56 + }`, 56, false}, + {`kubernetes cluster.local`, defaultTTL, false}, + {`kubernetes cluster.local { + ttl -1 + }`, 0, true}, + {`kubernetes cluster.local { + ttl 3601 + }`, 0, true}, + } + + for i, tc := range tests { + c := caddy.NewTestController("dns", tc.input) + k, _, err := kubernetesParse(c) + if err != nil && !tc.shouldErr { + t.Fatalf("Test %d: Expected no error, got %q", i, err) + } + if err == nil && tc.shouldErr { + t.Fatalf("Test %d: Expected error, got none", i) + } + if err != nil && tc.shouldErr { + // input should error + continue + } + + if k.ttl != tc.expectedTTL { + t.Errorf("Test %d: Expected TTl to be %d, got %d", i, tc.expectedTTL, k.ttl) + } + } +} diff --git a/test/kubernetes_test.go b/test/kubernetes_test.go index 49d37734c..fa4751166 100644 --- a/test/kubernetes_test.go +++ b/test/kubernetes_test.go @@ -30,7 +30,7 @@ var dnsTestCases = []test.Case{ Qname: "svc-1-a.test-1.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.A("svc-1-a.test-1.svc.cluster.local. 303 IN A 10.0.0.100"), + test.A("svc-1-a.test-1.svc.cluster.local. 5 IN A 10.0.0.100"), }, }, { @@ -535,9 +535,7 @@ var dnsTestCasesFallthrough = []test.Case{ Rcode: dns.RcodeSuccess, Answer: append(srvResponse("_c-port._UDP.*.test-1.svc.cluster.local.", "TypeSRV", "headless-svc", "test-1"), []dns.RR{ - test.SRV("_c-port._UDP.*.test-1.svc.cluster.local. 303 IN SRV 0 33 1234 svc-c.test-1.svc.cluster.local."), - }...), - + test.SRV("_c-port._UDP.*.test-1.svc.cluster.local. 303 IN SRV 0 33 1234 svc-c.test-1.svc.cluster.local.")}...), Extra: append(srvResponse("_c-port._UDP.*.test-1.svc.cluster.local.", "TypeA", "headless-svc", "test-1"), []dns.RR{ test.A("svc-c.test-1.svc.cluster.local. 303 IN A 10.0.0.115"), @@ -626,14 +624,14 @@ var dnsTestCasesFallthrough = []test.Case{ Rcode: dns.RcodeSuccess, Answer: []dns.RR{ test.A("example.net. 303 IN A 13.14.15.16"), - test.CNAME("ext-svc.test-1.svc.cluster.local. 0 IN CNAME example.net."), + test.CNAME("ext-svc.test-1.svc.cluster.local. 303 IN CNAME example.net."), }, }, { Qname: "ext-svc.test-1.svc.cluster.local.", Qtype: dns.TypeCNAME, Rcode: dns.RcodeSuccess, Answer: []dns.RR{ - test.CNAME("ext-svc.test-1.svc.cluster.local. 0 IN CNAME example.net."), + test.CNAME("ext-svc.test-1.svc.cluster.local. 303 IN CNAME example.net."), }, }, { @@ -855,7 +853,7 @@ func srvResponse(qname, responsetype, namespace, name string) []dns.RR { ip := strings.Replace(result[i], ".", "-", -1) t := strconv.Itoa(100 / (lr + 1)) if responsetype == "TypeA" { - rr = append(rr, test.A(ip+"."+namespace+"."+name+".svc.cluster.local. 0 IN A "+result[i])) + rr = append(rr, test.A(ip+"."+namespace+"."+name+".svc.cluster.local. 303 IN A "+result[i])) } if responsetype == "TypeSRV" && namespace == "headless-svc" { rr = append(rr, test.SRV(qname+" 303 IN SRV 0 "+t+" 1234 "+ip+"."+namespace+"."+name+".svc.cluster.local.")) @@ -864,7 +862,6 @@ func srvResponse(qname, responsetype, namespace, name string) []dns.RR { rr = append(rr, test.SRV(qname+" 303 IN SRV 0 "+t+" 443 "+ip+"."+namespace+"."+name+".svc.cluster.local.")) rr = append(rr, test.SRV(qname+" 303 IN SRV 0 "+t+" 80 "+ip+"."+namespace+"."+name+".svc.cluster.local.")) } - } return rr }