diff --git a/middleware/kubernetes/federation.go b/middleware/kubernetes/federation.go index 6f7e5f122..4c8d20cfe 100644 --- a/middleware/kubernetes/federation.go +++ b/middleware/kubernetes/federation.go @@ -62,13 +62,13 @@ func (k *Kubernetes) federationCNAMERecord(r recordRequest) msg.Service { } if r.endpoint == "" { return msg.Service{ - Key: strings.Join([]string{msg.Path(r.zone, "coredns"), r.typeName, r.federation, r.namespace, r.service}, "/"), - Host: strings.Join([]string{r.service, r.namespace, r.federation, r.typeName, node.Labels[labelAvailabilityZone], node.Labels[labelRegion], f.zone}, "."), + Key: strings.Join([]string{msg.Path(r.zone, "coredns"), r.podOrSvc, r.federation, r.namespace, r.service}, "/"), + Host: strings.Join([]string{r.service, r.namespace, r.federation, r.podOrSvc, node.Labels[labelAvailabilityZone], node.Labels[labelRegion], f.zone}, "."), } } return msg.Service{ - Key: strings.Join([]string{msg.Path(r.zone, "coredns"), r.typeName, r.federation, r.namespace, r.service, r.endpoint}, "/"), - Host: strings.Join([]string{r.endpoint, r.service, r.namespace, r.federation, r.typeName, node.Labels[labelAvailabilityZone], node.Labels[labelRegion], f.zone}, "."), + Key: strings.Join([]string{msg.Path(r.zone, "coredns"), r.podOrSvc, r.federation, r.namespace, r.service, r.endpoint}, "/"), + Host: strings.Join([]string{r.endpoint, r.service, r.namespace, r.federation, r.podOrSvc, node.Labels[labelAvailabilityZone], node.Labels[labelRegion], f.zone}, "."), } } diff --git a/middleware/kubernetes/federation_test.go b/middleware/kubernetes/federation_test.go index eae3d74d4..be5069c26 100644 --- a/middleware/kubernetes/federation_test.go +++ b/middleware/kubernetes/federation_test.go @@ -85,17 +85,17 @@ func testFederationCNAMERecord(t *testing.T, k Kubernetes, input recordRequest, } func TestFederationCNAMERecord(t *testing.T) { - k := Kubernetes{Zones: []string{"inter.webs"}} + k := Kubernetes{Zones: []string{"inter.webs."}} k.Federations = []Federation{{name: "fed", zone: "era.tion.com"}} k.APIConn = apiConnFedTest{} k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("10.9.8.7") } - r, _ := k.parseRequest("s1.ns.fed.svc.inter.webs", dns.TypeA) + r, _ := k.parseRequest("s1.ns.fed.svc.inter.webs.", dns.TypeA, "inter.webs.") testFederationCNAMERecord(t, k, r, msg.Service{Key: "/coredns/webs/inter/svc/fed/ns/s1", Host: "s1.ns.fed.svc.fd-az.fd-r.era.tion.com"}) - r, _ = k.parseRequest("ep1.s1.ns.fed.svc.inter.webs", dns.TypeA) + r, _ = k.parseRequest("ep1.s1.ns.fed.svc.inter.webs.", dns.TypeA, "inter.webs.") testFederationCNAMERecord(t, k, r, msg.Service{Key: "/coredns/webs/inter/svc/fed/ns/s1/ep1", Host: "ep1.s1.ns.fed.svc.fd-az.fd-r.era.tion.com"}) - r, _ = k.parseRequest("ep1.s1.ns.foo.svc.inter.webs", dns.TypeA) + r, _ = k.parseRequest("ep1.s1.ns.foo.svc.inter.webs.", dns.TypeA, "inter.webs.") testFederationCNAMERecord(t, k, r, msg.Service{Key: "", Host: ""}) } diff --git a/middleware/kubernetes/handler.go b/middleware/kubernetes/handler.go index 520ab0344..868b39d7f 100644 --- a/middleware/kubernetes/handler.go +++ b/middleware/kubernetes/handler.go @@ -34,10 +34,13 @@ func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.M if !k.isRequestInReverseRange(state.Name()) { return middleware.NextOrFailure(k.Name(), k.Next, ctx, w, r) } - // Set the zone to this specific request. + + // Set the zone to this specific request, as we want to handle this reverse request. zone = state.Name() } + state.Zone = zone + var ( records []dns.RR extra []dns.RR diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 1b07d30d5..417436904 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -11,6 +11,7 @@ import ( "github.com/coredns/coredns/middleware" "github.com/coredns/coredns/middleware/etcd/msg" + "github.com/coredns/coredns/middleware/pkg/dnsutil" dnsstrings "github.com/coredns/coredns/middleware/pkg/strings" "github.com/coredns/coredns/middleware/proxy" "github.com/coredns/coredns/request" @@ -64,7 +65,8 @@ type endpoint struct { port api.EndpointPort } -type service struct { +// kService is a service as retrieved via the k8s API. +type kService struct { name string namespace string addr string @@ -72,23 +74,13 @@ type service struct { endpoints []endpoint } -type pod struct { +// kPod is a pod as retrieved via the k8s API. +type kPod struct { name string namespace string addr string } -type recordRequest struct { - port string - protocol string - endpoint string - service string - namespace string - typeName string - zone string - federation string -} - var ( errNoItems = errors.New("no items found") errNsNotExposed = errors.New("namespace is not exposed") @@ -100,12 +92,33 @@ var ( // Services implements the ServiceBackend interface. 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.QType()) + + // We're looking again at types, which we've already done in ServeDNS, but there are some types k8s just can't answer. + switch state.QType() { + case dns.TypeTXT: + // 1 label + zone, label must be "dns-version" + t, err := dnsutil.TrimZone(state.Name(), state.Zone) + if err != nil { + return nil, nil, err + } + segs := dns.SplitDomainName(t) + if len(segs) != 1 { + return nil, nil, errors.New("servfail") + } + if segs[0] != "dns-version" { + return nil, nil, errInvalidRequest + } + svc := msg.Service{Text: DNSSchemaVersion, TTL: 28800, Key: msg.Path(state.QName(), "coredns")} + return []msg.Service{svc}, nil, nil + } + + r, e := k.parseRequest(state.Name(), state.QType(), state.Zone) if e != nil { return nil, nil, e } - switch state.Type() { - case "A", "AAAA", "CNAME": + + switch state.QType() { + case dns.TypeA, dns.TypeAAAA, dns.TypeCNAME: 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 @@ -118,7 +131,7 @@ func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware. return nil, nil, e } return s, nil, e // Haven't implemented debug queries yet. - case "SRV": + case dns.TypeSRV: s, e := k.Entries(r) // SRV for external services is not yet implemented, so remove those records noext := []msg.Service{} @@ -128,13 +141,7 @@ func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware. } } return noext, nil, e - case "TXT": - if r.typeName == "dns-version" { - srv := k.recordsForTXT(r) - svcs = append(svcs, srv) - } - return svcs, nil, err - case "NS": + case dns.TypeNS: srv := k.recordsForNS(r) svcs = append(svcs, srv) return svcs, nil, err @@ -142,11 +149,6 @@ func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware. return nil, nil, nil } -func (k *Kubernetes) recordsForTXT(r recordRequest) msg.Service { - return msg.Service{Text: DNSSchemaVersion, TTL: 28800, - Key: msg.Path(strings.Join([]string{r.typeName, r.zone}, "."), "coredns")} -} - func (k *Kubernetes) recordsForNS(r recordRequest) msg.Service { ns := k.coreDNSRecord() return msg.Service{Host: ns.A.String(), @@ -234,99 +236,6 @@ func (k *Kubernetes) InitKubeCache() (err error) { return err } -func (k *Kubernetes) parseRequest(lowerCasedName string, qtype uint16) (r recordRequest, err error) { - // 3 Possible cases - // SRV Request: _port._protocol.service.namespace.[federation.]type.zone - // A Request (endpoint): endpoint.service.namespace.[federation.]type.zone - // A Request (service): service.namespace.[federation.]type.zone - - // separate zone from rest of lowerCasedName - var segs []string - for _, z := range k.Zones { - if dns.IsSubDomain(z, lowerCasedName) { - r.zone = z - - segs = dns.SplitDomainName(lowerCasedName) - segs = segs[:len(segs)-dns.CountLabel(r.zone)] - break - } - } - if r.zone == "" { - return r, errZoneNotFound - } - - r.federation, segs = k.stripFederation(segs) - - if qtype == dns.TypeNS { - return r, nil - } - - if qtype == dns.TypeA && isDefaultNS(lowerCasedName, r) { - return r, nil - } - - offset := 0 - if qtype == dns.TypeSRV { - // The kubernetes peer-finder expects queries with empty port and service to resolve - // If neither is specified, treat it as a wildcard - if len(segs) == 3 { - r.port = "*" - r.service = "*" - offset = 0 - } else { - if len(segs) != 5 { - return r, errInvalidRequest - } - // This is a SRV style request, get first two elements as port and - // protocol, stripping leading underscores if present. - if segs[0][0] == '_' { - r.port = segs[0][1:] - } else { - r.port = segs[0] - if !wildcard(r.port) { - return r, errInvalidRequest - } - } - if segs[1][0] == '_' { - r.protocol = segs[1][1:] - if r.protocol != "tcp" && r.protocol != "udp" { - return r, errInvalidRequest - } - } else { - r.protocol = segs[1] - if !wildcard(r.protocol) { - return r, errInvalidRequest - } - } - if r.port == "" || r.protocol == "" { - return r, errInvalidRequest - } - offset = 2 - } - } - if (qtype == dns.TypeA || qtype == dns.TypeAAAA) && len(segs) == 4 { - // This is an endpoint A/AAAA record request. Get first element as endpoint. - r.endpoint = segs[0] - offset = 1 - } - - if len(segs) == (offset + 3) { - r.service = segs[offset] - r.namespace = segs[offset+1] - r.typeName = segs[offset+2] - - return r, nil - } - - if len(segs) == 1 && qtype == dns.TypeTXT { - r.typeName = segs[0] - return r, nil - } - - return r, errInvalidRequest - -} - // Records not implemented, see Entries(). func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { return nil, fmt.Errorf("NOOP") @@ -375,7 +284,7 @@ func endpointHostname(addr api.EndpointAddress) string { return "" } -func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, r recordRequest) (records []msg.Service) { +func (k *Kubernetes) getRecordsForK8sItems(services []kService, pods []kPod, r recordRequest) (records []msg.Service) { zonePath := msg.Path(r.zone, "coredns") for _, svc := range services { @@ -435,7 +344,7 @@ func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, r rec return records } -func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) { +func (k *Kubernetes) findPods(namespace, podname string) (pods []kPod, err error) { if k.PodMode == PodModeDisabled { return pods, errPodsDisabled } @@ -448,7 +357,7 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) } if k.PodMode == PodModeInsecure { - s := pod{name: podname, namespace: namespace, addr: ip} + s := kPod{name: podname, namespace: namespace, addr: ip} pods = append(pods, s) return pods, nil } @@ -468,7 +377,7 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) } // check for matching ip and namespace if ip == p.Status.PodIP && match(namespace, p.Namespace, nsWildcard) { - s := pod{name: podname, namespace: namespace, addr: ip} + s := kPod{name: podname, namespace: namespace, addr: ip} pods = append(pods, s) return pods, nil } @@ -477,9 +386,9 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) } // get retrieves matching data from the cache. -func (k *Kubernetes) get(r recordRequest) (services []service, pods []pod, err error) { +func (k *Kubernetes) get(r recordRequest) (services []kService, pods []kPod, err error) { switch { - case r.typeName == Pod: + case r.podOrSvc == Pod: pods, err = k.findPods(r.namespace, r.service) return nil, pods, err default: @@ -488,9 +397,9 @@ func (k *Kubernetes) get(r recordRequest) (services []service, pods []pod, err e } } -func (k *Kubernetes) findServices(r recordRequest) ([]service, error) { +func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) { serviceList := k.APIConn.ServiceList() - var resultItems []service + var resultItems []kService nsWildcard := wildcard(r.namespace) serviceWildcard := wildcard(r.service) @@ -506,7 +415,7 @@ func (k *Kubernetes) findServices(r recordRequest) ([]service, error) { if nsWildcard && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(svc.Namespace, k.Namespaces)) { continue } - s := service{name: svc.Name, namespace: svc.Namespace} + s := kService{name: svc.Name, namespace: svc.Namespace} // Endpoint query or headless service if svc.Spec.ClusterIP == api.ClusterIPNone || r.endpoint != "" { diff --git a/middleware/kubernetes/kubernetes_test.go b/middleware/kubernetes/kubernetes_test.go index a19cb16f6..8037c0987 100644 --- a/middleware/kubernetes/kubernetes_test.go +++ b/middleware/kubernetes/kubernetes_test.go @@ -1,8 +1,6 @@ package kubernetes import ( - "errors" - "reflect" "testing" "github.com/coredns/coredns/middleware" @@ -12,17 +10,6 @@ import ( "k8s.io/client-go/1.5/pkg/api" ) -func TestRecordForTXT(t *testing.T) { - k := Kubernetes{Zones: []string{"inter.webs.test"}} - r, _ := k.parseRequest("dns-version.inter.webs.test", dns.TypeTXT) - - expected := DNSSchemaVersion - svc := k.recordsForTXT(r) - if svc.Text != expected { - t.Errorf("Expected result '%v'. Instead got result '%v'.", expected, svc.Text) - } -} - func TestPrimaryZone(t *testing.T) { k := Kubernetes{Zones: []string{"inter.webs.test", "inter.nets.test"}} expected := "inter.webs.test" @@ -32,23 +19,6 @@ func TestPrimaryZone(t *testing.T) { } } -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 occurred") - if k.IsNameError(otherErr) { - t.Errorf("Expected 'true' for '%v'", otherErr) - } -} - func TestWildcard(t *testing.T) { var tests = []struct { s string @@ -70,151 +40,6 @@ func TestWildcard(t *testing.T) { } } -func expectString(t *testing.T, function, qtype, query string, r *recordRequest, field, expected string) { - ref := reflect.ValueOf(r) - refField := reflect.Indirect(ref).FieldByName(field) - got := refField.String() - if got != expected { - t.Errorf("Expected %v(%v, \"%v\") to get %v == \"%v\". Instead got \"%v\".", function, query, qtype, field, expected, got) - } -} - -func TestParseRequest(t *testing.T) { - - var tcs map[string]string - - k := Kubernetes{Zones: []string{"inter.webs.test"}} - f := "parseRequest" - - // Test a valid SRV request - // - query := "_http._tcp.webs.mynamespace.svc.inter.webs.test." - r, e := k.parseRequest(query, dns.TypeSRV) - if e != nil { - t.Errorf("Expected no error from parseRequest(%v, \"SRV\"). Instead got '%v'.", query, e) - } - - tcs = map[string]string{ - "port": "http", - "protocol": "tcp", - "endpoint": "", - "service": "webs", - "namespace": "mynamespace", - "typeName": Svc, - "zone": "inter.webs.test", - } - for field, expected := range tcs { - expectString(t, f, "SRV", query, &r, field, expected) - } - - // Test wildcard acceptance - // - query = "*.any.*.any.svc.inter.webs.test." - r, e = k.parseRequest(query, dns.TypeSRV) - if e != nil { - t.Errorf("Expected no error from parseRequest(\"%v\", \"SRV\"). Instead got '%v'.", query, e) - } - - tcs = map[string]string{ - "port": "*", - "protocol": "any", - "endpoint": "", - "service": "*", - "namespace": "any", - "typeName": Svc, - "zone": "inter.webs.test", - } - for field, expected := range tcs { - expectString(t, f, "SRV", query, &r, field, expected) - } - - // Test A request of endpoint - query = "1-2-3-4.webs.mynamespace.svc.inter.webs.test." - r, e = k.parseRequest(query, dns.TypeA) - if e != nil { - t.Errorf("Expected no error from parseRequest(\"%v\", \"A\"). Instead got '%v'.", query, e) - } - tcs = map[string]string{ - "port": "", - "protocol": "", - "endpoint": "1-2-3-4", - "service": "webs", - "namespace": "mynamespace", - "typeName": Svc, - "zone": "inter.webs.test", - } - for field, expected := range tcs { - 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 - "servname.ns1.srv.inter.nets.test.", // A requests must have zone that matches corefile - - } - for _, q := range invalidAQueries { - _, e = k.parseRequest(q, dns.TypeA) - if e == nil { - t.Errorf("Expected error from %v(\"%v\", \"A\").", f, q) - } - } - - invalidSRVQueries := []string{ - "_http._pcp.webs.mynamespace.svc.inter.webs.test.", // SRV protocol must be tcp or udp - "_http._tcp.ep.webs.ns.svc.inter.webs.test.", // SRV requests cannot have an endpoint - "_*._*.webs.mynamespace.svc.inter.webs.test.", // SRV request with invalid wildcards - "_http._tcp", - "_tcp.test.", - ".", - } - - for _, q := range invalidSRVQueries { - _, 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 @@ -384,7 +209,7 @@ func (APIConnServiceTest) GetNodeByName(name string) (api.Node, error) { func TestServices(t *testing.T) { - k := Kubernetes{Zones: []string{"interwebs.test"}} + k := Kubernetes{Zones: []string{"interwebs.test."}} k.Federations = []Federation{{name: "fed", zone: "era.tion.com"}} k.interfaceAddrsFunc = localPodIP k.APIConn = &APIConnServiceTest{} @@ -414,7 +239,8 @@ func TestServices(t *testing.T) { for _, test := range tests { state := request.Request{ - Req: &dns.Msg{Question: []dns.Question{{Name: test.qname, Qtype: test.qtype}}}, + Req: &dns.Msg{Question: []dns.Question{{Name: test.qname, Qtype: test.qtype}}}, + Zone: "interwebs.test.", // must match from k.Zones[0] } svcs, _, e := k.Services(state, false, middleware.Options{}) if e != nil { diff --git a/middleware/kubernetes/ns_test.go b/middleware/kubernetes/ns_test.go index 1aa4f910d..b7e2ae513 100644 --- a/middleware/kubernetes/ns_test.go +++ b/middleware/kubernetes/ns_test.go @@ -7,10 +7,10 @@ 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"}} + 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) + r, _ := k.parseRequest("inter.webs.test.", dns.TypeNS, "inter.webs.test.") expected := "/coredns/test/webs/inter/kube-system/coredns" svc := k.recordsForNS(r) @@ -20,10 +20,10 @@ func TestRecordForNS(t *testing.T) { } func TestDefaultNSMsg(t *testing.T) { - k := Kubernetes{Zones: []string{"inter.webs.test"}} + 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) + r, _ := k.parseRequest("ns.dns.inter.webs.test.", dns.TypeA, "inter.webs.test.") expected := "/coredns/test/webs/inter/dns/ns" svc := k.defaultNSMsg(r) @@ -33,13 +33,13 @@ func TestDefaultNSMsg(t *testing.T) { } func TestIsDefaultNS(t *testing.T) { - k := Kubernetes{Zones: []string{"inter.webs.test"}} - r, _ := k.parseRequest("ns.dns.inter.webs.test", dns.TypeA) + k := Kubernetes{Zones: []string{"inter.webs.test."}} + r, _ := k.parseRequest("ns.dns.inter.webs.test", dns.TypeA, "inter.webs.test.") var name string var expected bool - name = "ns.dns.inter.webs.test" + name = "ns.dns.inter.webs.test." expected = true if isDefaultNS(name, r) != expected { t.Errorf("Expected IsDefaultNS('%v') to be '%v'.", name, expected) diff --git a/middleware/kubernetes/parse.go b/middleware/kubernetes/parse.go new file mode 100644 index 000000000..790060896 --- /dev/null +++ b/middleware/kubernetes/parse.go @@ -0,0 +1,120 @@ +package kubernetes + +import ( + "github.com/coredns/coredns/middleware/pkg/dnsutil" + + "github.com/miekg/dns" +) + +type recordRequest struct { + // The named port from the kubernetes DNS spec, this is the service part (think _https) from a well formed + // SRV record. + port string + // The protocol is usually _udp or _tcp (if set), and comes from the protocol part of a well formed + // SRV record. + protocol string + endpoint string + service string + namespace string + // A each name can be for a pod or a service, here we track what we've seen. This value is true for + // pods and false for services. If we ever need to extend this well use a typed value. + podOrSvc string + zone string + federation string +} + +// TODO(miek): make it use request.Request. +func (k *Kubernetes) parseRequest(lowerCasedName string, qtype uint16, zone ...string) (r recordRequest, err error) { + // 3 Possible cases + // SRV Request: _port._protocol.service.namespace.[federation.]type.zone + // A Request (endpoint): endpoint.service.namespace.[federation.]type.zone + // A Request (service): service.namespace.[federation.]type.zone + + if len(zone) == 0 { + panic("parseRequest must be called with a zone") + } + + base, _ := dnsutil.TrimZone(lowerCasedName, zone[0]) + segs := dns.SplitDomainName(base) + + r.zone = zone[0] + r.federation, segs = k.stripFederation(segs) + + if qtype == dns.TypeNS { + return r, nil + } + + if qtype == dns.TypeA && isDefaultNS(lowerCasedName, r) { + return r, nil + } + + offset := 0 + if qtype == dns.TypeSRV { + // The kubernetes peer-finder expects queries with empty port and service to resolve + // If neither is specified, treat it as a wildcard + if len(segs) == 3 { + r.port = "*" + r.service = "*" + offset = 0 + } else { + if len(segs) != 5 { + return r, errInvalidRequest + } + // This is a SRV style request, get first two elements as port and + // protocol, stripping leading underscores if present. + if segs[0][0] == '_' { + r.port = segs[0][1:] + } else { + r.port = segs[0] + if !wildcard(r.port) { + return r, errInvalidRequest + } + } + if segs[1][0] == '_' { + r.protocol = segs[1][1:] + if r.protocol != "tcp" && r.protocol != "udp" { + return r, errInvalidRequest + } + } else { + r.protocol = segs[1] + if !wildcard(r.protocol) { + return r, errInvalidRequest + } + } + if r.port == "" || r.protocol == "" { + return r, errInvalidRequest + } + offset = 2 + } + } + if (qtype == dns.TypeA || qtype == dns.TypeAAAA) && len(segs) == 4 { + // This is an endpoint A/AAAA record request. Get first element as endpoint. + r.endpoint = segs[0] + offset = 1 + } + + if len(segs) == (offset + 3) { + r.service = segs[offset] + r.namespace = segs[offset+1] + r.podOrSvc = segs[offset+2] + + return r, nil + } + + return r, errInvalidRequest +} + +// String return a string representation of r, it just returns all +// fields concatenated with dots. +// This is mostly used in tests. +func (r recordRequest) String() string { + s := r.port + s += "." + r.protocol + s += "." + r.endpoint + s += "." + r.service + s += "." + r.namespace + s += "." + r.podOrSvc + s += "." + r.zone + s += "." + r.federation + return s +} diff --git a/middleware/kubernetes/parse_test.go b/middleware/kubernetes/parse_test.go new file mode 100644 index 000000000..06c3711cd --- /dev/null +++ b/middleware/kubernetes/parse_test.go @@ -0,0 +1,131 @@ +package kubernetes + +import ( + "reflect" + "testing" + + "github.com/miekg/dns" +) + +func expectString(t *testing.T, function, qtype, query string, r *recordRequest, field, expected string) { + ref := reflect.ValueOf(r) + refField := reflect.Indirect(ref).FieldByName(field) + got := refField.String() + if got != expected { + t.Errorf("Expected %v(%v, \"%v\") to get %v == \"%v\". Instead got \"%v\".", function, query, qtype, field, expected, got) + } +} + +func TestParseRequest(t *testing.T) { + + var tcs map[string]string + + zone := "intern.webs.tests." + k := Kubernetes{Zones: []string{zone}} + f := "parseRequest" + + // Test a valid SRV request + // + query := "_http._tcp.webs.mynamespace.svc.inter.webs.test." + r, e := k.parseRequest(query, dns.TypeSRV, zone) + if e != nil { + t.Errorf("Expected no error from parseRequest(%v, \"SRV\"). Instead got '%v'.", query, e) + } + + tcs = map[string]string{ + "port": "http", + "protocol": "tcp", + "endpoint": "", + "service": "webs", + "namespace": "mynamespace", + "podOrSvc": Svc, + "zone": zone, + } + for field, expected := range tcs { + expectString(t, f, "SRV", query, &r, field, expected) + } + + // Test wildcard acceptance + // + query = "*.any.*.any.svc.inter.webs.test." + r, e = k.parseRequest(query, dns.TypeSRV, zone) + if e != nil { + t.Errorf("Expected no error from parseRequest(\"%v\", \"SRV\"). Instead got '%v'.", query, e) + } + + tcs = map[string]string{ + "port": "*", + "protocol": "any", + "endpoint": "", + "service": "*", + "namespace": "any", + "podOrSvc": Svc, + "zone": zone, + } + for field, expected := range tcs { + expectString(t, f, "SRV", query, &r, field, expected) + } + + // Test A request of endpoint + query = "1-2-3-4.webs.mynamespace.svc.inter.webs.test." + r, e = k.parseRequest(query, dns.TypeA, zone) + if e != nil { + t.Errorf("Expected no error from parseRequest(\"%v\", \"A\"). Instead got '%v'.", query, e) + } + tcs = map[string]string{ + "port": "", + "protocol": "", + "endpoint": "1-2-3-4", + "service": "webs", + "namespace": "mynamespace", + "podOrSvc": Svc, + "zone": zone, + } + for field, expected := range tcs { + expectString(t, f, "A", query, &r, field, expected) + } + + // Test NS request + query = "inter.webs.test." + r, e = k.parseRequest(query, dns.TypeNS, zone) + 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": "", + "podOrSvc": "", + "zone": zone, + } + for field, expected := range tcs { + expectString(t, f, "NS", query, &r, field, expected) + } + + // Invalid query tests + invalidAQueries := []string{ + "_http._tcp.webs.mynamespace.svc.inter.webs.test.", // A requests cannot have port or protocol TODO(miek): this must return NODATA + + } + for _, q := range invalidAQueries { + _, e = k.parseRequest(q, dns.TypeA, zone) + if e == nil { + t.Errorf("Expected error from %v(\"%v\", \"A\").", f, q) + } + } + + invalidSRVQueries := []string{ + "_http._pcp.webs.mynamespace.svc.inter.webs.test.", // SRV protocol must be tcp or udp + "_http._tcp.ep.webs.ns.svc.inter.webs.test.", // SRV requests cannot have an endpoint + "_*._*.webs.mynamespace.svc.inter.webs.test.", // SRV request with invalid wildcards + } + + for _, q := range invalidSRVQueries { + _, e = k.parseRequest(q, dns.TypeSRV, zone) + if e == nil { + t.Errorf("Expected error from %v(\"%v\", \"SRV\").", f, q) + } + } +} diff --git a/test/kubernetes_test.go b/test/kubernetes_test.go index ac5414743..1d00be3b8 100644 --- a/test/kubernetes_test.go +++ b/test/kubernetes_test.go @@ -543,7 +543,7 @@ func TestKubernetesIntegrationCidrReverseZone(t *testing.T) { kubernetes cluster.local { endpoint http://localhost:8080 namespaces test-1 - cidrs 10.0.0.0/24 + cidrs 10.0.0.0/24 } erratic . { drop 0 @@ -555,10 +555,10 @@ func TestKubernetesIntegrationCidrReverseZone(t *testing.T) { func TestKubernetesIntegrationPartialCidrReverseZone(t *testing.T) { corefile := `.:0 { - kubernetes cluster.local { + kubernetes cluster.local { endpoint http://localhost:8080 namespaces test-1 - cidrs 10.0.0.96/28 10.0.0.120/32 + cidrs 10.0.0.96/28 10.0.0.120/32 } erratic . { drop 0 @@ -572,7 +572,7 @@ func TestKubernetesIntegrationAllNSExposed(t *testing.T) { `.:0 { kubernetes cluster.local { endpoint http://localhost:8080 - cidrs 10.0.0.0/24 + cidrs 10.0.0.0/24 } ` doIntegrationTests(t, corefile, dnsTestCasesAllNSExposed)