Commit 3527be6c authored by Brian Akins's avatar Brian Akins Committed by John Belamaric

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
parent c6ce769f
...@@ -31,6 +31,7 @@ kubernetes [ZONES...] { ...@@ -31,6 +31,7 @@ kubernetes [ZONES...] {
namespaces NAMESPACE... namespaces NAMESPACE...
labels EXPRESSION labels EXPRESSION
pods POD-MODE pods POD-MODE
endpoint_pod_names
upstream ADDRESS... upstream ADDRESS...
ttl TTL ttl TTL
fallthrough fallthrough
...@@ -65,6 +66,16 @@ kubernetes [ZONES...] { ...@@ -65,6 +66,16 @@ kubernetes [ZONES...] {
option requires substantially more memory than in insecure mode, since it will maintain a watch option requires substantially more memory than in insecure mode, since it will maintain a watch
on all pods. 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 * `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 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. to a file structured like resolv.conf.
......
...@@ -28,19 +28,20 @@ import ( ...@@ -28,19 +28,20 @@ import (
// Kubernetes implements a plugin that connects to a Kubernetes cluster. // Kubernetes implements a plugin that connects to a Kubernetes cluster.
type Kubernetes struct { type Kubernetes struct {
Next plugin.Handler Next plugin.Handler
Zones []string Zones []string
Proxy proxy.Proxy // Proxy for looking up names during the resolution process Proxy proxy.Proxy // Proxy for looking up names during the resolution process
APIServerList []string APIServerList []string
APIProxy *apiProxy APIProxy *apiProxy
APICertAuth string APICertAuth string
APIClientCert string APIClientCert string
APIClientKey string APIClientKey string
APIConn dnsController APIConn dnsController
Namespaces map[string]bool Namespaces map[string]bool
podMode string podMode string
Fallthrough bool endpointNameMode bool
ttl uint32 Fallthrough bool
ttl uint32
primaryZoneIndex int primaryZoneIndex int
interfaceAddrsFunc func() net.IP interfaceAddrsFunc func() net.IP
...@@ -276,10 +277,13 @@ func (k *Kubernetes) Records(state request.Request, exact bool) ([]msg.Service, ...@@ -276,10 +277,13 @@ func (k *Kubernetes) Records(state request.Request, exact bool) ([]msg.Service,
return services, err return services, err
} }
func endpointHostname(addr api.EndpointAddress) string { func endpointHostname(addr api.EndpointAddress, endpointNameMode bool) string {
if addr.Hostname != "" { if addr.Hostname != "" {
return strings.ToLower(addr.Hostname) return strings.ToLower(addr.Hostname)
} }
if endpointNameMode && addr.TargetRef != nil && addr.TargetRef.Name != "" {
return addr.TargetRef.Name
}
if strings.Contains(addr.IP, ".") { if strings.Contains(addr.IP, ".") {
return strings.Replace(addr.IP, ".", "-", -1) return strings.Replace(addr.IP, ".", "-", -1)
} }
...@@ -375,7 +379,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. ...@@ -375,7 +379,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg.
// See comments in parse.go parseRequest about the endpoint handling. // See comments in parse.go parseRequest about the endpoint handling.
if r.endpoint != "" { if r.endpoint != "" {
if !match(r.endpoint, endpointHostname(addr)) { if !match(r.endpoint, endpointHostname(addr, k.endpointNameMode)) {
continue continue
} }
} }
...@@ -385,7 +389,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg. ...@@ -385,7 +389,7 @@ func (k *Kubernetes) findServices(r recordRequest, zone string) (services []msg.
continue continue
} }
s := msg.Service{Host: addr.IP, Port: int(p.Port), TTL: k.ttl} 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 err = nil
......
...@@ -34,15 +34,21 @@ func TestWildcard(t *testing.T) { ...@@ -34,15 +34,21 @@ func TestWildcard(t *testing.T) {
func TestEndpointHostname(t *testing.T) { func TestEndpointHostname(t *testing.T) {
var tests = []struct { var tests = []struct {
ip string ip string
hostname string hostname string
expected string expected string
podName string
endpointNameMode bool
}{ }{
{"10.11.12.13", "", "10-11-12-13"}, {"10.11.12.13", "", "10-11-12-13", "", false},
{"10.11.12.13", "epname", "epname"}, {"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 { 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 { 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) t.Errorf("Expected endpoint name for (ip:%v hostname:%v) to be '%v', but got '%v'", test.ip, test.hostname, test.expected, result)
} }
......
...@@ -42,7 +42,7 @@ func (k *Kubernetes) serviceRecordForIP(ip, name string) []msg.Service { ...@@ -42,7 +42,7 @@ func (k *Kubernetes) serviceRecordForIP(ip, name string) []msg.Service {
for _, eps := range ep.Subsets { for _, eps := range ep.Subsets {
for _, addr := range eps.Addresses { for _, addr := range eps.Addresses {
if addr.IP == ip { 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}} return []msg.Service{{Host: domain}}
} }
} }
......
...@@ -104,6 +104,13 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, dnsControlOpts, error) { ...@@ -104,6 +104,13 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, dnsControlOpts, error) {
for c.NextBlock() { for c.NextBlock() {
switch c.Val() { 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": case "pods":
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) == 1 { if len(args) == 1 {
......
...@@ -471,3 +471,66 @@ func TestKubernetesParse(t *testing.T) { ...@@ -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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment