From 7f950e496ab8ae518d8f78f3a80225604dd90da3 Mon Sep 17 00:00:00 2001 From: Chris O'Haver Date: Mon, 22 May 2017 16:05:48 -0400 Subject: [PATCH] Handle K8s middleware NS record (#662) * commit for testing in cluster * commit for testing in cluster * refactor and add ns.dns record * Release 007 * reduce heap allocations * gofmt * revert accidental Makefile commits * restore prior rcode for disabled pod mode * revert Makefile deltas * add unit tests * more unit tests * make isRequestInReverseRange easier to test * more unit tests * addressing review feedback * commit setup.go --- middleware/kubernetes/controller.go | 46 ++++-- middleware/kubernetes/handler.go | 2 +- middleware/kubernetes/kubernetes.go | 141 ++++++++++-------- middleware/kubernetes/kubernetes_test.go | 181 ++++++++++++++++++++--- middleware/kubernetes/lookup.go | 2 +- middleware/kubernetes/ns.go | 115 ++++++++++++++ middleware/kubernetes/ns_test.go | 133 +++++++++++++++++ middleware/kubernetes/setup.go | 7 +- test/kubernetes_test.go | 7 + 9 files changed, 537 insertions(+), 97 deletions(-) create mode 100644 middleware/kubernetes/ns.go create mode 100644 middleware/kubernetes/ns_test.go diff --git a/middleware/kubernetes/controller.go b/middleware/kubernetes/controller.go index e6e79366a..892f99d33 100644 --- a/middleware/kubernetes/controller.go +++ b/middleware/kubernetes/controller.go @@ -35,7 +35,15 @@ func (s *storeToNamespaceLister) List() (ns api.NamespaceList, err error) { return ns, nil } -type dnsController struct { +type dnsController interface { + ServiceList() []*api.Service + PodIndex(string) []interface{} + EndpointsList() api.EndpointsList + Run() + Stop() error +} + +type dnsControl struct { client *kubernetes.Clientset selector *labels.Selector @@ -59,8 +67,8 @@ type dnsController struct { } // newDNSController creates a controller for CoreDNS. -func newdnsController(kubeClient *kubernetes.Clientset, resyncPeriod time.Duration, lselector *labels.Selector, initPodCache bool) *dnsController { - dns := dnsController{ +func newdnsController(kubeClient *kubernetes.Clientset, resyncPeriod time.Duration, lselector *labels.Selector, initPodCache bool) *dnsControl { + dns := dnsControl{ client: kubeClient, selector: lselector, stopCh: make(chan struct{}), @@ -286,12 +294,12 @@ func endpointsWatchFunc(c *kubernetes.Clientset, ns string, s *labels.Selector) } } -func (dns *dnsController) controllersInSync() bool { +func (dns *dnsControl) controllersInSync() bool { return dns.svcController.HasSynced() } // Stop stops the controller. -func (dns *dnsController) Stop() error { +func (dns *dnsControl) Stop() error { dns.stopLock.Lock() defer dns.stopLock.Unlock() @@ -307,7 +315,7 @@ func (dns *dnsController) Stop() error { } // Run starts the controller. -func (dns *dnsController) Run() { +func (dns *dnsControl) Run() { go dns.svcController.Run(dns.stopCh) go dns.nsController.Run(dns.stopCh) go dns.epController.Run(dns.stopCh) @@ -317,7 +325,7 @@ func (dns *dnsController) Run() { <-dns.stopCh } -func (dns *dnsController) NamespaceList() *api.NamespaceList { +func (dns *dnsControl) NamespaceList() *api.NamespaceList { nsList, err := dns.nsLister.List() if err != nil { return &api.NamespaceList{} @@ -326,7 +334,7 @@ func (dns *dnsController) NamespaceList() *api.NamespaceList { return &nsList } -func (dns *dnsController) ServiceList() []*api.Service { +func (dns *dnsControl) ServiceList() []*api.Service { svcs, err := dns.svcLister.List(labels.Everything()) if err != nil { return []*api.Service{} @@ -335,10 +343,28 @@ func (dns *dnsController) ServiceList() []*api.Service { return svcs } +func (dns *dnsControl) PodIndex(ip string) []interface{} { + pods, err := dns.podLister.Indexer.ByIndex(podIPIndex, ip) + if err != nil { + return nil + } + + return pods +} + +func (dns *dnsControl) EndpointsList() api.EndpointsList { + epl, err := dns.epLister.List() + if err != nil { + return api.EndpointsList{} + } + + return epl +} + // ServicesByNamespace returns a map of: // // namespacename :: [ kubernetesService ] -func (dns *dnsController) ServicesByNamespace() map[string][]api.Service { +func (dns *dnsControl) ServicesByNamespace() map[string][]api.Service { k8sServiceList := dns.ServiceList() items := make(map[string][]api.Service, len(k8sServiceList)) for _, i := range k8sServiceList { @@ -350,7 +376,7 @@ func (dns *dnsController) ServicesByNamespace() map[string][]api.Service { } // ServiceInNamespace returns the Service that matches servicename in the namespace -func (dns *dnsController) ServiceInNamespace(namespace, servicename string) *api.Service { +func (dns *dnsControl) ServiceInNamespace(namespace, servicename string) *api.Service { svcObj, err := dns.svcLister.Services(namespace).Get(servicename) if err != nil { // TODO(...): should return err here diff --git a/middleware/kubernetes/handler.go b/middleware/kubernetes/handler.go index 6eb637506..ec1184198 100644 --- a/middleware/kubernetes/handler.go +++ b/middleware/kubernetes/handler.go @@ -32,7 +32,7 @@ func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.M // If this is a PTR request, and the request is in a defined // pod/service cidr range, process the request in this middleware, // otherwise pass to next middleware. - if !k.isRequestInReverseRange(state) { + if !k.isRequestInReverseRange(state.Name()) { return middleware.NextOrFailure(k.Name(), k.Next, ctx, w, r) } // Set the zone to this specific request. diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 01e0c9115..5b7b7bc3c 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -28,22 +28,23 @@ import ( // Kubernetes implements a middleware that connects to a Kubernetes cluster. type Kubernetes struct { - Next middleware.Handler - Zones []string - primaryZone int - Proxy proxy.Proxy // Proxy for looking up names during the resolution process - APIEndpoint string - APICertAuth string - APIClientCert string - APIClientKey string - APIConn *dnsController - ResyncPeriod time.Duration - Namespaces []string - LabelSelector *unversionedapi.LabelSelector - Selector *labels.Selector - PodMode string - ReverseCidrs []net.IPNet - Fallthrough bool + Next middleware.Handler + Zones []string + primaryZone int + Proxy proxy.Proxy // Proxy for looking up names during the resolution process + APIEndpoint string + APICertAuth string + APIClientCert string + APIClientKey string + APIConn dnsController + ResyncPeriod time.Duration + Namespaces []string + LabelSelector *unversionedapi.LabelSelector + Selector *labels.Selector + PodMode string + ReverseCidrs []net.IPNet + Fallthrough bool + interfaceAddrs InterfaceAddrser } const ( @@ -83,36 +84,49 @@ type recordRequest struct { var errNoItems = errors.New("no items found") var errNsNotExposed = errors.New("namespace is not exposed") var errInvalidRequest = errors.New("invalid query name") +var errZoneNotFound = errors.New("zone not found") +var errApiBadPodType = errors.New("expected type *api.Pod") +var errPodsDisabled = errors.New("pod records disabled") // Services implements the ServiceBackend interface. -func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.Options) ([]msg.Service, []msg.Service, error) { +func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.Options) (svcs []msg.Service, debug []msg.Service, err error) { - r, e := k.parseRequest(state.Name(), state.Type()) + r, e := k.parseRequest(state.Name(), state.QType()) if e != nil { return nil, nil, e } switch state.Type() { case "A", "SRV": + if state.Type() == "A" && isDefaultNS(state.Name(), r) { + // If this is an A request for "ns.dns", respond with a "fake" record for coredns. + // SOA records always use this hardcoded name + svcs = append(svcs, k.defaultNSMsg(r)) + return svcs, nil, nil + } s, e := k.Records(r) return s, nil, e // Haven't implemented debug queries yet. case "TXT": - s, e := k.recordsForTXT(r) - return s, nil, e + err := k.recordsForTXT(r, &svcs) + return svcs, nil, err + case "NS": + err = k.recordsForNS(r, &svcs) + return svcs, nil, err } return nil, nil, nil } -func (k *Kubernetes) recordsForTXT(r recordRequest) ([]msg.Service, error) { +func (k *Kubernetes) recordsForTXT(r recordRequest, svcs *[]msg.Service) (err error) { switch r.typeName { case "dns-version": s := msg.Service{ Text: DNSSchemaVersion, TTL: 28800, - Key: msg.Path(r.typeName+"."+r.zone, "coredns")} - return []msg.Service{s}, nil + Key: msg.Path(strings.Join([]string{r.typeName, r.zone}, "."), "coredns")} + *svcs = append(*svcs, s) + return nil } - return nil, nil + return nil } // PrimaryZone will return the first non-reverse zone being handled by this middleware @@ -122,6 +136,7 @@ func (k *Kubernetes) PrimaryZone() string { // Reverse implements the ServiceBackend interface. func (k *Kubernetes) Reverse(state request.Request, exact bool, opt middleware.Options) ([]msg.Service, []msg.Service, error) { + ip := dnsutil.ExtractAddressFromReverse(state.Name()) if ip == "" { return nil, nil, nil @@ -131,8 +146,8 @@ func (k *Kubernetes) Reverse(state request.Request, exact bool, opt middleware.O return records, nil, nil } -func (k *Kubernetes) isRequestInReverseRange(state request.Request) bool { - ip := dnsutil.ExtractAddressFromReverse(state.Name()) +func (k *Kubernetes) isRequestInReverseRange(name string) bool { + ip := dnsutil.ExtractAddressFromReverse(name) for _, c := range k.ReverseCidrs { if c.Contains(net.ParseIP(ip)) { return true @@ -186,7 +201,8 @@ func (k *Kubernetes) getClientConfig() (*rest.Config, error) { } // InitKubeCache initializes a new Kubernetes cache. -func (k *Kubernetes) InitKubeCache() error { + +func (k *Kubernetes) InitKubeCache() (err error) { config, err := k.getClientConfig() if err != nil { @@ -216,12 +232,11 @@ func (k *Kubernetes) InitKubeCache() error { return err } -func (k *Kubernetes) parseRequest(lowerCasedName, qtype string) (r recordRequest, err error) { +func (k *Kubernetes) parseRequest(lowerCasedName string, qtype uint16) (r recordRequest, err error) { // 3 Possible cases // SRV Request: _port._protocol.service.namespace.type.zone // A Request (endpoint): endpoint.service.namespace.type.zone // A Request (service): service.namespace.type.zone - // separate zone from rest of lowerCasedName var segs []string for _, z := range k.Zones { @@ -234,11 +249,19 @@ func (k *Kubernetes) parseRequest(lowerCasedName, qtype string) (r recordRequest } } if r.zone == "" { - return r, errors.New("zone not found") + return r, errZoneNotFound + } + + if qtype == dns.TypeNS { + return r, nil + } + + if qtype == dns.TypeA && isDefaultNS(lowerCasedName, r) { + return r, nil } offset := 0 - if qtype == "SRV" { + if qtype == dns.TypeSRV { if len(segs) != 5 { return r, errInvalidRequest } @@ -268,7 +291,7 @@ func (k *Kubernetes) parseRequest(lowerCasedName, qtype string) (r recordRequest } offset = 2 } - if qtype == "A" && len(segs) == 4 { + if qtype == dns.TypeA && len(segs) == 4 { // This is an endpoint A record request. Get first element as endpoint. r.endpoint = segs[0] offset = 1 @@ -282,7 +305,7 @@ func (k *Kubernetes) parseRequest(lowerCasedName, qtype string) (r recordRequest return r, nil } - if len(segs) == 1 && qtype == "TXT" { + if len(segs) == 1 && qtype == dns.TypeTXT { r.typeName = segs[0] return r, nil } @@ -328,37 +351,35 @@ func endpointHostname(addr api.EndpointAddress) string { return "" } -func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, zone string) []msg.Service { - var records []msg.Service +func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, zone string) (records []msg.Service) { + zonePath := msg.Path(zone, "coredns") for _, svc := range services { - - key := svc.name + "." + svc.namespace + ".svc." + zone - if svc.addr == api.ClusterIPNone { // This is a headless service, create records for each endpoint for _, ep := range svc.endpoints { - ephostname := endpointHostname(ep.addr) s := msg.Service{ - Key: msg.Path(strings.ToLower(ephostname+"."+key), "coredns"), - Host: ep.addr.IP, Port: int(ep.port.Port), + Key: strings.Join([]string{zonePath, "svc", svc.namespace, svc.name, endpointHostname(ep.addr)}, "/"), + Host: ep.addr.IP, + Port: int(ep.port.Port), } records = append(records, s) - } } else { // Create records for each exposed port... for _, p := range svc.ports { - s := msg.Service{Key: msg.Path(strings.ToLower(key), "coredns"), Host: svc.addr, Port: int(p.Port)} + s := msg.Service{ + Key: strings.Join([]string{zonePath, "svc", svc.namespace, svc.name}, "/"), + Host: svc.addr, + Port: int(p.Port)} records = append(records, s) } } } for _, p := range pods { - key := p.name + "." + p.namespace + ".pod." + zone s := msg.Service{ - Key: msg.Path(strings.ToLower(key), "coredns"), + Key: strings.Join([]string{zonePath, "pod", p.namespace, p.name}, "/"), Host: p.addr, } records = append(records, s) @@ -376,7 +397,7 @@ func ipFromPodName(podname string) string { func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) { if k.PodMode == PodModeDisabled { - return pods, errors.New("pod records disabled") + return pods, errPodsDisabled } var ip string @@ -393,16 +414,13 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) } // PodModeVerified - objList, err := k.APIConn.podLister.Indexer.ByIndex(podIPIndex, ip) - if err != nil { - return nil, err - } + objList := k.APIConn.PodIndex(ip) nsWildcard := symbolContainsWildcard(namespace) for _, o := range objList { p, ok := o.(*api.Pod) if !ok { - return nil, errors.New("expected type *api.Pod") + return nil, errApiBadPodType } // If namespace has a wildcard, filter results against Corefile namespace list. if nsWildcard && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(p.Namespace, k.Namespaces)) { @@ -461,10 +479,8 @@ func (k *Kubernetes) findServices(r recordRequest) ([]service, error) { continue } // Headless service - endpointsList, err := k.APIConn.epLister.List() - if err != nil { - continue - } + endpointsList := k.APIConn.EndpointsList() + for _, ep := range endpointsList.Items { if ep.ObjectMeta.Name != svc.Name || ep.ObjectMeta.Namespace != svc.Namespace { continue @@ -500,24 +516,19 @@ func symbolMatches(queryString, candidateString string, wildcard bool) bool { // If a service cluster ip does not match, it checks all endpoints func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service { // First check services with cluster ips - svcList, err := k.APIConn.svcLister.List(labels.Everything()) - if err != nil { - return nil - } + svcList := k.APIConn.ServiceList() + for _, service := range svcList { if (len(k.Namespaces) > 0) && !dnsstrings.StringInSlice(service.Namespace, k.Namespaces) { continue } if service.Spec.ClusterIP == ip { - domain := service.Name + "." + service.Namespace + ".svc." + k.PrimaryZone() + domain := strings.Join([]string{service.Name, service.Namespace, "svc", k.PrimaryZone()}, ".") return []msg.Service{{Host: domain}} } } // If no cluster ips match, search endpoints - epList, err := k.APIConn.epLister.List() - if err != nil { - return nil - } + epList := k.APIConn.EndpointsList() for _, ep := range epList.Items { if (len(k.Namespaces) > 0) && !dnsstrings.StringInSlice(ep.ObjectMeta.Namespace, k.Namespaces) { continue @@ -525,7 +536,7 @@ func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service { for _, eps := range ep.Subsets { for _, addr := range eps.Addresses { if addr.IP == ip { - domain := endpointHostname(addr) + "." + ep.ObjectMeta.Name + "." + ep.ObjectMeta.Namespace + ".svc." + k.PrimaryZone() + domain := strings.Join([]string{endpointHostname(addr), ep.ObjectMeta.Name, ep.ObjectMeta.Namespace, "svc", k.PrimaryZone()}, ".") return []msg.Service{{Host: domain}} } } diff --git a/middleware/kubernetes/kubernetes_test.go b/middleware/kubernetes/kubernetes_test.go index 2b83aaecf..4f748565a 100644 --- a/middleware/kubernetes/kubernetes_test.go +++ b/middleware/kubernetes/kubernetes_test.go @@ -1,22 +1,95 @@ package kubernetes -import "testing" -import "reflect" +import ( + "errors" + "net" + "reflect" + "testing" -// Test data for TestSymbolContainsWildcard cases. -var testdataSymbolContainsWildcard = []struct { - Symbol string - ExpectedResult bool -}{ - {"mynamespace", false}, - {"*", true}, - {"any", true}, - {"my*space", false}, - {"*space", false}, - {"myname*", false}, + "github.com/miekg/dns" + "k8s.io/client-go/1.5/pkg/api" + + "github.com/coredns/coredns/middleware/etcd/msg" +) + +func TestRecordForTXT(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test"}} + r, _ := k.parseRequest("dns-version.inter.webs.test", dns.TypeTXT) + expected := DNSSchemaVersion + + var svcs []msg.Service + k.recordsForTXT(r, &svcs) + if svcs[0].Text != expected { + t.Errorf("Expected result '%v'. Instead got result '%v'.", expected, svcs[0].Text) + } +} + +func TestPrimaryZone(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test", "inter.nets.test"}} + expected := "inter.webs.test" + result := k.PrimaryZone() + if result != expected { + t.Errorf("Expected result '%v'. Instead got result '%v'.", expected, result) + } +} + +func TestIsRequestInReverseRange(t *testing.T) { + + tests := []struct { + cidr string + name string + expected bool + }{ + {"1.2.3.0/24", "4.3.2.1.in-addr.arpa.", true}, + {"1.2.3.0/24", "5.3.2.1.in-addr.arpa.", true}, + {"1.2.3.0/24", "5.4.2.1.in-addr.arpa.", false}, + {"5.6.0.0/16", "5.4.2.1.in-addr.arpa.", false}, + {"5.6.0.0/16", "5.4.6.5.in-addr.arpa.", true}, + {"5.6.0.0/16", "5.6.0.1.in-addr.arpa.", false}, + } + + k := Kubernetes{Zones: []string{"inter.webs.test"}} + + for _, test := range tests { + _, cidr, _ := net.ParseCIDR(test.cidr) + k.ReverseCidrs = []net.IPNet{*cidr} + result := k.isRequestInReverseRange(test.name) + if result != test.expected { + t.Errorf("Expected '%v' for '%v' in %v.", test.expected, test.name, test.cidr) + } + } +} + +func TestIsNameError(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test"}} + if !k.IsNameError(errNoItems) { + t.Errorf("Expected 'true' for '%v'", errNoItems) + } + if !k.IsNameError(errNsNotExposed) { + t.Errorf("Expected 'true' for '%v'", errNsNotExposed) + } + if !k.IsNameError(errInvalidRequest) { + t.Errorf("Expected 'true' for '%v'", errInvalidRequest) + } + otherErr := errors.New("Some other error occured") + if k.IsNameError(otherErr) { + t.Errorf("Expected 'true' for '%v'", otherErr) + } } func TestSymbolContainsWildcard(t *testing.T) { + var testdataSymbolContainsWildcard = []struct { + Symbol string + ExpectedResult bool + }{ + {"mynamespace", false}, + {"*", true}, + {"any", true}, + {"my*space", false}, + {"*space", false}, + {"myname*", false}, + } + for _, example := range testdataSymbolContainsWildcard { actualResult := symbolContainsWildcard(example.Symbol) if actualResult != example.ExpectedResult { @@ -44,7 +117,7 @@ func TestParseRequest(t *testing.T) { // Test a valid SRV request // query := "_http._tcp.webs.mynamespace.svc.inter.webs.test." - r, e := k.parseRequest(query, "SRV") + r, e := k.parseRequest(query, dns.TypeSRV) if e != nil { t.Errorf("Expected no error from parseRequest(%v, \"SRV\"). Instead got '%v'.", query, e) } @@ -65,7 +138,7 @@ func TestParseRequest(t *testing.T) { // Test wildcard acceptance // query = "*.any.*.any.svc.inter.webs.test." - r, e = k.parseRequest(query, "SRV") + r, e = k.parseRequest(query, dns.TypeSRV) if e != nil { t.Errorf("Expected no error from parseRequest(\"%v\", \"SRV\"). Instead got '%v'.", query, e) } @@ -85,7 +158,7 @@ func TestParseRequest(t *testing.T) { // Test A request of endpoint query = "1-2-3-4.webs.mynamespace.svc.inter.webs.test." - r, e = k.parseRequest(query, "A") + r, e = k.parseRequest(query, dns.TypeA) if e != nil { t.Errorf("Expected no error from parseRequest(\"%v\", \"A\"). Instead got '%v'.", query, e) } @@ -102,6 +175,44 @@ func TestParseRequest(t *testing.T) { expectString(t, f, "A", query, &r, field, expected) } + // Test NS request + query = "inter.webs.test." + r, e = k.parseRequest(query, dns.TypeNS) + if e != nil { + t.Errorf("Expected no error from parseRequest(\"%v\", \"NS\"). Instead got '%v'.", query, e) + } + tcs = map[string]string{ + "port": "", + "protocol": "", + "endpoint": "", + "service": "", + "namespace": "", + "typeName": "", + "zone": "inter.webs.test", + } + for field, expected := range tcs { + expectString(t, f, "NS", query, &r, field, expected) + } + + // Test TXT request + query = "dns-version.inter.webs.test." + r, e = k.parseRequest(query, dns.TypeTXT) + if e != nil { + t.Errorf("Expected no error from parseRequest(\"%v\", \"TXT\"). Instead got '%v'.", query, e) + } + tcs = map[string]string{ + "port": "", + "protocol": "", + "endpoint": "", + "service": "", + "namespace": "", + "typeName": "dns-version", + "zone": "inter.webs.test", + } + for field, expected := range tcs { + expectString(t, f, "TXT", query, &r, field, expected) + } + // Invalid query tests invalidAQueries := []string{ "_http._tcp.webs.mynamespace.svc.inter.webs.test.", // A requests cannot have port or protocol @@ -109,7 +220,7 @@ func TestParseRequest(t *testing.T) { } for _, q := range invalidAQueries { - _, e = k.parseRequest(q, "A") + _, e = k.parseRequest(q, dns.TypeA) if e == nil { t.Errorf("Expected error from %v(\"%v\", \"A\").", f, q) } @@ -126,9 +237,43 @@ func TestParseRequest(t *testing.T) { } for _, q := range invalidSRVQueries { - _, e = k.parseRequest(q, "SRV") + _, e = k.parseRequest(q, dns.TypeSRV) if e == nil { t.Errorf("Expected error from %v(\"%v\", \"SRV\").", f, q) } } } + +func TestEndpointHostname(t *testing.T) { + var tests = []struct { + ip string + hostname string + expected string + }{ + {"10.11.12.13", "", "10-11-12-13"}, + {"10.11.12.13", "epname", "epname"}, + } + for _, test := range tests { + result := endpointHostname(api.EndpointAddress{IP: test.ip, Hostname: test.hostname}) + 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) + } + } +} + +func TestIpFromPodName(t *testing.T) { + var tests = []struct { + ip string + expected string + }{ + {"10-11-12-13", "10.11.12.13"}, + {"1-2-3-4", "1.2.3.4"}, + {"1-2-3--A-B-C", "1:2:3::A:B:C"}, + } + for _, test := range tests { + result := ipFromPodName(test.ip) + if result != test.expected { + t.Errorf("Expected ip for podname '%v' to be '%v', but got '%v'", test.ip, test.expected, result) + } + } +} diff --git a/middleware/kubernetes/lookup.go b/middleware/kubernetes/lookup.go index 57659012d..e47b6e8c6 100644 --- a/middleware/kubernetes/lookup.go +++ b/middleware/kubernetes/lookup.go @@ -12,7 +12,7 @@ import ( ) func (k Kubernetes) records(state request.Request, exact bool) ([]msg.Service, error) { - r, err := k.parseRequest(state.Name(), state.Type()) + r, err := k.parseRequest(state.Name(), state.QType()) if err != nil { return nil, err } diff --git a/middleware/kubernetes/ns.go b/middleware/kubernetes/ns.go new file mode 100644 index 000000000..7f7736abc --- /dev/null +++ b/middleware/kubernetes/ns.go @@ -0,0 +1,115 @@ +package kubernetes + +import ( + "net" + "strings" + + "github.com/coredns/coredns/middleware/etcd/msg" + + "github.com/miekg/dns" + "k8s.io/client-go/1.5/pkg/api" +) + +const DefaultNSName = "ns.dns." + +var corednsRecord dns.A + +type InterfaceAddrser interface { + InterfaceAddrs() ([]net.Addr, error) +} + +type InterfaceAddrs struct{} + +func (i InterfaceAddrs) InterfaceAddrs() ([]net.Addr, error) { + return net.InterfaceAddrs() +} + +func (k *Kubernetes) recordsForNS(r recordRequest, svcs *[]msg.Service) error { + ns := k.CoreDNSRecord() + s := msg.Service{ + Host: ns.A.String(), + Key: msg.Path(strings.Join([]string{ns.Hdr.Name, r.zone}, "."), "coredns")} + *svcs = append(*svcs, s) + return nil +} + +// DefaultNSMsg returns an msg.Service representing an A record for +// ns.dns.[zone] -> dns service ip. This A record is needed to legitimize +// the SOA response in middleware.NS(), which is hardcoded at ns.dns.[zone]. +func (k *Kubernetes) defaultNSMsg(r recordRequest) msg.Service { + ns := k.CoreDNSRecord() + s := msg.Service{ + Key: msg.Path(strings.Join([]string{DefaultNSName, r.zone}, "."), "coredns"), + Host: ns.A.String(), + } + return s +} + +func isDefaultNS(name string, r recordRequest) bool { + return strings.Index(name, DefaultNSName) == 0 && strings.Index(name, r.zone) == len(DefaultNSName) +} + +func (k *Kubernetes) CoreDNSRecord() dns.A { + var localIP net.IP + var svcName string + var svcNamespace string + var dnsIP net.IP + + if len(corednsRecord.Hdr.Name) == 0 || corednsRecord.A == nil { + // get local Pod IP + addrs, _ := k.interfaceAddrs.InterfaceAddrs() + + for _, addr := range addrs { + ip, _, _ := net.ParseCIDR(addr.String()) + ip = ip.To4() + + if ip == nil || ip.IsLoopback() { + continue + } + localIP = ip + break + } + // Find endpoint matching IP to get service and namespace + endpointsList := k.APIConn.EndpointsList() + + FindEndpoint: + for _, ep := range endpointsList.Items { + for _, eps := range ep.Subsets { + for _, addr := range eps.Addresses { + if localIP.Equal(net.ParseIP(addr.IP)) { + + svcNamespace = ep.ObjectMeta.Namespace + svcName = ep.ObjectMeta.Name + break FindEndpoint + } + } + } + } + + if len(svcName) == 0 { + corednsRecord.Hdr.Name = DefaultNSName + corednsRecord.A = localIP + return corednsRecord + } + // Find service to get ClusterIP + serviceList := k.APIConn.ServiceList() + FindService: + for _, svc := range serviceList { + if svcName == svc.Name && svcNamespace == svc.Namespace { + if svc.Spec.ClusterIP == api.ClusterIPNone { + dnsIP = localIP + } else { + dnsIP = net.ParseIP(svc.Spec.ClusterIP) + } + break FindService + } + } + if dnsIP == nil { + dnsIP = localIP + } + + corednsRecord.Hdr.Name = strings.Join([]string{svcName, svcNamespace, "svc."}, ".") + corednsRecord.A = dnsIP + } + return corednsRecord +} diff --git a/middleware/kubernetes/ns_test.go b/middleware/kubernetes/ns_test.go new file mode 100644 index 000000000..193eb056a --- /dev/null +++ b/middleware/kubernetes/ns_test.go @@ -0,0 +1,133 @@ +package kubernetes + +import "testing" +import "net" + +import "github.com/coredns/coredns/middleware/etcd/msg" +import "k8s.io/client-go/1.5/pkg/api" +import "github.com/miekg/dns" + +func TestRecordForNS(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test"}} + corednsRecord.Hdr.Name = "coredns.kube-system." + corednsRecord.A = net.IP("1.2.3.4") + r, _ := k.parseRequest("inter.webs.test", dns.TypeNS) + expected := "/coredns/test/webs/inter/kube-system/coredns" + + var svcs []msg.Service + k.recordsForNS(r, &svcs) + if svcs[0].Key != expected { + t.Errorf("Expected result '%v'. Instead got result '%v'.", expected, svcs[0].Key) + } +} + +func TestDefaultNSMsg(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test"}} + corednsRecord.Hdr.Name = "coredns.kube-system." + corednsRecord.A = net.IP("1.2.3.4") + r, _ := k.parseRequest("ns.dns.inter.webs.test", dns.TypeA) + expected := "/coredns/test/webs/inter/dns/ns" + + svc := k.defaultNSMsg(r) + if svc.Key != expected { + t.Errorf("Expected result '%v'. Instead got result '%v'.", expected, svc.Key) + } +} + +func TestIsDefaultNS(t *testing.T) { + k := Kubernetes{Zones: []string{"inter.webs.test"}} + r, _ := k.parseRequest("ns.dns.inter.webs.test", dns.TypeA) + + var name string + var expected bool + + name = "ns.dns.inter.webs.test" + expected = true + if isDefaultNS(name, r) != expected { + t.Errorf("Expected IsDefaultNS('%v') to be '%v'.", name, expected) + } + name = "ns.dns.blah.inter.webs.test" + expected = false + if isDefaultNS(name, r) != expected { + t.Errorf("Expected IsDefaultNS('%v') to be '%v'.", name, expected) + } +} + +type APIConnTest struct{} + +func (APIConnTest) Run() { + return +} + +func (APIConnTest) Stop() error { + return nil +} + +func (APIConnTest) ServiceList() []*api.Service { + svc := api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "dns-service", + Namespace: "kube-system", + }, + Spec: api.ServiceSpec{ + ClusterIP: "10.0.0.111", + }, + } + + return []*api.Service{&svc} + +} + +func (APIConnTest) PodIndex(string) []interface{} { + return nil +} + +func (APIConnTest) EndpointsList() api.EndpointsList { + return api.EndpointsList{ + Items: []api.Endpoints{ + { + Subsets: []api.EndpointSubset{ + { + Addresses: []api.EndpointAddress{ + { + IP: "172.0.40.10", + }, + }, + }, + }, + ObjectMeta: api.ObjectMeta{ + Name: "dns-service", + Namespace: "kube-system", + }, + }, + }, + } +} + +type InterfaceAddrsTest struct{} + +func (i InterfaceAddrsTest) InterfaceAddrs() ([]net.Addr, error) { + _, ipnet, _ := net.ParseCIDR("172.0.40.10/32") + return []net.Addr{ipnet}, nil +} + +func TestDoCoreDNSRecord(t *testing.T) { + + corednsRecord = dns.A{} + k := Kubernetes{Zones: []string{"inter.webs.test"}} + + k.interfaceAddrs = &InterfaceAddrsTest{} + k.APIConn = &APIConnTest{} + + cdr := k.CoreDNSRecord() + + expected := "10.0.0.111" + + if cdr.A.String() != expected { + t.Errorf("Expected A to be '%v', got '%v'", expected, cdr.A.String()) + } + expected = "dns-service.kube-system.svc." + if cdr.Hdr.Name != expected { + t.Errorf("Expected Hdr.Name to be '%v', got '%v'", expected, cdr.Hdr.Name) + } +} diff --git a/middleware/kubernetes/setup.go b/middleware/kubernetes/setup.go index 9467d1d28..23300733f 100644 --- a/middleware/kubernetes/setup.go +++ b/middleware/kubernetes/setup.go @@ -51,8 +51,11 @@ func setup(c *caddy.Controller) error { } func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { - k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod} - k8s.PodMode = PodModeDisabled + k8s := &Kubernetes{ + ResyncPeriod: defaultResyncPeriod, + interfaceAddrs: &InterfaceAddrs{}, + PodMode: PodModeDisabled, + } for c.Next() { if c.Val() == "kubernetes" { diff --git a/test/kubernetes_test.go b/test/kubernetes_test.go index 4ef4e18ec..5390d0c7d 100644 --- a/test/kubernetes_test.go +++ b/test/kubernetes_test.go @@ -233,6 +233,13 @@ var dnsTestCases = []test.Case{ test.A("next-in-chain. 0 IN A 192.0.2.53"), }, }, + { + Qname: "cluster.local.", Qtype: dns.TypeNS, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.NS("cluster.local. 0 IN NS kubernetes.default.svc.cluster.local."), + }, + }, } var dnsTestCasesPodsInsecure = []test.Case{