From 3527be6c0056acbcd8dc9848ff96be2136c68ed5 Mon Sep 17 00:00:00 2001 From: Brian Akins Date: Wed, 8 Nov 2017 08:07:10 -0500 Subject: [PATCH] Add option to use pod name rather than IP address for Kubernetes (#1190) Change to use a new 'endpoints' directive and use a constant Add initial docs for 'endpoints' directive Add tests to Kubernetes setup for endpoints Changes based on PR feedback endpoint_pod_names is a boolean config option. Chahanged docs to reflect this. Add a test when endpoints_pod_names is not set Update README.md Remove endpointNameModeName as it is no longer used --- plugin/kubernetes/README.md | 11 +++++ plugin/kubernetes/kubernetes.go | 36 +++++++++------- plugin/kubernetes/kubernetes_test.go | 18 +++++--- plugin/kubernetes/reverse.go | 2 +- plugin/kubernetes/setup.go | 7 ++++ plugin/kubernetes/setup_test.go | 63 ++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/plugin/kubernetes/README.md b/plugin/kubernetes/README.md index a2eabe97f..358b0b307 100644 --- a/plugin/kubernetes/README.md +++ b/plugin/kubernetes/README.md @@ -31,6 +31,7 @@ kubernetes [ZONES...] { namespaces NAMESPACE... labels EXPRESSION pods POD-MODE + endpoint_pod_names upstream ADDRESS... ttl TTL fallthrough @@ -65,6 +66,16 @@ kubernetes [ZONES...] { option requires substantially more memory than in insecure mode, since it will maintain a watch on all pods. +* `endpoint_pod_names` Use the pod name of the pod targeted by the endpoint as + the endpoint name in A records, e.g. + `endpoint-name.my-service.namespace.svc.cluster.local. in A 1.2.3.4` + By default, the endpoint-name name selection is as follows: Use the hostname + of the endpoint, or if hostname is not set, use the dashed form of the endpoint + ip address (e.g. `1-2-3-4.my-service.namespace.svc.cluster.local.`) + If this directive is included, then name selection for endpoints changes as + follows: Use the hostname of the endpoint, or if hostname is not set, use the + pod name of the pod targeted by the endpoint. If there is no pod targeted by + the endpoint, use the dashed ip address form. * `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. diff --git a/plugin/kubernetes/kubernetes.go b/plugin/kubernetes/kubernetes.go index 23b8f1b00..ad89e1686 100644 --- a/plugin/kubernetes/kubernetes.go +++ b/plugin/kubernetes/kubernetes.go @@ -28,19 +28,20 @@ import ( // Kubernetes implements a plugin that connects to a Kubernetes cluster. type Kubernetes struct { - Next plugin.Handler - Zones []string - Proxy proxy.Proxy // Proxy for looking up names during the resolution process - APIServerList []string - APIProxy *apiProxy - APICertAuth string - APIClientCert string - APIClientKey string - APIConn dnsController - Namespaces map[string]bool - podMode string - Fallthrough bool - ttl uint32 + Next plugin.Handler + Zones []string + Proxy proxy.Proxy // Proxy for looking up names during the resolution process + APIServerList []string + APIProxy *apiProxy + APICertAuth string + APIClientCert string + APIClientKey string + APIConn dnsController + Namespaces map[string]bool + podMode string + endpointNameMode bool + Fallthrough bool + ttl uint32 primaryZoneIndex int interfaceAddrsFunc func() net.IP @@ -276,10 +277,13 @@ func (k *Kubernetes) Records(state request.Request, exact bool) ([]msg.Service, return services, err } -func endpointHostname(addr api.EndpointAddress) string { +func endpointHostname(addr api.EndpointAddress, endpointNameMode bool) string { if addr.Hostname != "" { return strings.ToLower(addr.Hostname) } + if endpointNameMode && addr.TargetRef != nil && addr.TargetRef.Name != "" { + return addr.TargetRef.Name + } if strings.Contains(addr.IP, ".") { return strings.Replace(addr.IP, ".", "-", -1) } @@ -375,7 +379,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. // See comments in parse.go parseRequest about the endpoint handling. if r.endpoint != "" { - if !match(r.endpoint, endpointHostname(addr)) { + if !match(r.endpoint, endpointHostname(addr, k.endpointNameMode)) { continue } } @@ -385,7 +389,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. continue } 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)}, "/") + s.Key = strings.Join([]string{zonePath, Svc, svc.Namespace, svc.Name, endpointHostname(addr, k.endpointNameMode)}, "/") err = nil diff --git a/plugin/kubernetes/kubernetes_test.go b/plugin/kubernetes/kubernetes_test.go index ae0bb13e5..de8c3f025 100644 --- a/plugin/kubernetes/kubernetes_test.go +++ b/plugin/kubernetes/kubernetes_test.go @@ -34,15 +34,21 @@ func TestWildcard(t *testing.T) { func TestEndpointHostname(t *testing.T) { var tests = []struct { - ip string - hostname string - expected string + ip string + hostname string + expected string + podName string + endpointNameMode bool }{ - {"10.11.12.13", "", "10-11-12-13"}, - {"10.11.12.13", "epname", "epname"}, + {"10.11.12.13", "", "10-11-12-13", "", false}, + {"10.11.12.13", "epname", "epname", "", false}, + {"10.11.12.13", "", "10-11-12-13", "hello-abcde", false}, + {"10.11.12.13", "epname", "epname", "hello-abcde", false}, + {"10.11.12.13", "epname", "epname", "hello-abcde", true}, + {"10.11.12.13", "", "hello-abcde", "hello-abcde", true}, } for _, test := range tests { - result := endpointHostname(api.EndpointAddress{IP: test.ip, Hostname: test.hostname}) + result := endpointHostname(api.EndpointAddress{IP: test.ip, Hostname: test.hostname, TargetRef: &api.ObjectReference{Name: test.podName}}, test.endpointNameMode) if result != test.expected { t.Errorf("Expected endpoint name for (ip:%v hostname:%v) to be '%v', but got '%v'", test.ip, test.hostname, test.expected, result) } diff --git a/plugin/kubernetes/reverse.go b/plugin/kubernetes/reverse.go index a67f59a5f..13cc78b8f 100644 --- a/plugin/kubernetes/reverse.go +++ b/plugin/kubernetes/reverse.go @@ -42,7 +42,7 @@ func (k *Kubernetes) serviceRecordForIP(ip, name string) []msg.Service { for _, eps := range ep.Subsets { for _, addr := range eps.Addresses { if addr.IP == ip { - domain := strings.Join([]string{endpointHostname(addr), ep.ObjectMeta.Name, ep.ObjectMeta.Namespace, Svc, k.primaryZone()}, ".") + domain := strings.Join([]string{endpointHostname(addr, k.endpointNameMode), ep.ObjectMeta.Name, ep.ObjectMeta.Namespace, Svc, k.primaryZone()}, ".") return []msg.Service{{Host: domain}} } } diff --git a/plugin/kubernetes/setup.go b/plugin/kubernetes/setup.go index 13e6c810f..fd63d6ff6 100644 --- a/plugin/kubernetes/setup.go +++ b/plugin/kubernetes/setup.go @@ -104,6 +104,13 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, dnsControlOpts, error) { for c.NextBlock() { switch c.Val() { + case "endpoint_pod_names": + args := c.RemainingArgs() + if len(args) > 0 { + return nil, opts, c.ArgErr() + } + k8s.endpointNameMode = true + continue case "pods": args := c.RemainingArgs() if len(args) == 1 { diff --git a/plugin/kubernetes/setup_test.go b/plugin/kubernetes/setup_test.go index 3f15ead06..224c168a1 100644 --- a/plugin/kubernetes/setup_test.go +++ b/plugin/kubernetes/setup_test.go @@ -471,3 +471,66 @@ func TestKubernetesParse(t *testing.T) { } } } + +func TestKubernetesEndpointsParse(t *testing.T) { + tests := []struct { + 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. + expectedEndpointMode bool + }{ + // valid endpoints mode + { + `kubernetes coredns.local { + endpoint_pod_names +}`, + false, + "", + true, + }, + // endpoints invalid + { + `kubernetes coredns.local { + endpoint_pod_names giant_seed +}`, + true, + "rong argument count or unexpected", + false, + }, + // endpoint not set + { + `kubernetes coredns.local { +}`, + false, + "", + false, + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + k8sController, _, err := kubernetesParse(c) + + 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 !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 + } + + // Endpoints + foundEndpointNameMode := k8sController.endpointNameMode + if foundEndpointNameMode != test.expectedEndpointMode { + t.Errorf("Test %d: Expected kubernetes controller to be initialized with endpoints mode '%v'. Instead found endpoints mode '%v' for input '%s'", i, test.expectedEndpointMode, foundEndpointNameMode, test.input) + } + } +}