Add option to use pod name rather than IP address for Kubernetes ()

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
This commit is contained in:
Brian Akins 2017-11-08 08:07:10 -05:00 committed by John Belamaric
parent c6ce769fc6
commit 3527be6c00
6 changed files with 114 additions and 23 deletions

View file

@ -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.

View file

@ -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

View file

@ -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)
}

View file

@ -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}}
}
}

View file

@ -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 {

View file

@ -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)
}
}
}