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
This commit is contained in:
parent
01f6e8cba5
commit
4049ed4f4b
6 changed files with 90 additions and 27 deletions
|
@ -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.
|
||||
|
|
|
@ -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)": {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
45
middleware/kubernetes/setup_ttl_test.go
Normal file
45
middleware/kubernetes/setup_ttl_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue