diff --git a/middleware/kubernetes/handler_test.go b/middleware/kubernetes/handler_test.go index 88a892404..1cdc8b619 100644 --- a/middleware/kubernetes/handler_test.go +++ b/middleware/kubernetes/handler_test.go @@ -12,6 +12,7 @@ import ( ) var dnsTestCases = map[string](test.Case){ + // *.any.svc-1-a.*.svc.cluster.local., "A Service": { Qname: "svc1.testns.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, @@ -19,6 +20,27 @@ var dnsTestCases = map[string](test.Case){ test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), }, }, + "A Service (wildcard)": { + Qname: "svc1.*.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), + }, + }, + "SRV Service (wildcards)": { + Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeSRV, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), + }, + }, + "A Service (wildcards)": { + Qname: "*.any.svc1.*.svc.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("svc1.testns.svc.cluster.local. 0 IN A 10.0.0.1"), + }, + }, "A Service (Headless)": { Qname: "hdls1.testns.svc.cluster.local.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 0ed71b6c0..3ac4293a5 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -14,7 +14,6 @@ import ( "github.com/coredns/coredns/middleware/etcd/msg" "github.com/coredns/coredns/middleware/pkg/dnsutil" "github.com/coredns/coredns/middleware/pkg/healthcheck" - dnsstrings "github.com/coredns/coredns/middleware/pkg/strings" "github.com/coredns/coredns/middleware/proxy" "github.com/coredns/coredns/request" @@ -40,7 +39,7 @@ type Kubernetes struct { APIClientKey string APIConn dnsController ResyncPeriod time.Duration - Namespaces []string + Namespaces map[string]bool LabelSelector *unversionedapi.LabelSelector Selector *labels.Selector PodMode string @@ -56,7 +55,11 @@ type Kubernetes struct { func New(zones []string) *Kubernetes { k := new(Kubernetes) k.Zones = zones + k.Namespaces = make(map[string]bool) k.interfaceAddrsFunc = func() net.IP { return net.ParseIP("127.0.0.1") } + k.PodMode = PodModeDisabled + k.Proxy = proxy.Proxy{} + k.ResyncPeriod = defaultResyncPeriod return k } @@ -303,7 +306,7 @@ func (k *Kubernetes) Entries(state request.Request) ([]msg.Service, error) { return nil, e } - if !k.namespaceExposed(r.namespace) { + if !wildcard(r.namespace) && !k.namespaceExposed(r.namespace) { return nil, errNsNotExposed } @@ -392,18 +395,17 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []kPod, err error // PodModeVerified objList := k.APIConn.PodIndex(ip) - nsWildcard := wildcard(namespace) for _, o := range objList { p, ok := o.(*api.Pod) if !ok { 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)) { + if wildcard(namespace) && !k.namespaceExposed(p.Namespace) { continue } // check for matching ip and namespace - if ip == p.Status.PodIP && match(namespace, p.Namespace, nsWildcard) { + if ip == p.Status.PodIP && match(namespace, p.Namespace) { s := kPod{name: podname, namespace: namespace, addr: ip} pods = append(pods, s) return pods, nil @@ -414,8 +416,8 @@ func (k *Kubernetes) findPods(namespace, podname string) (pods []kPod, err error // get retrieves matching data from the cache. func (k *Kubernetes) get(r recordRequest) (services []kService, pods []kPod, err error) { - switch { - case r.podOrSvc == Pod: + switch r.podOrSvc { + case Pod: pods, err = k.findPods(r.namespace, r.service) return nil, pods, err default: @@ -428,19 +430,14 @@ func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) { serviceList := k.APIConn.ServiceList() var resultItems []kService - nsWildcard := wildcard(r.namespace) - serviceWildcard := wildcard(r.service) - portWildcard := wildcard(r.port) || r.port == "" - protocolWildcard := wildcard(r.protocol) || r.protocol == "" - for _, svc := range serviceList { - if !(match(r.namespace, svc.Namespace, nsWildcard) && match(r.service, svc.Name, serviceWildcard)) { + if !(match(r.namespace, svc.Namespace) && match(r.service, svc.Name)) { continue } // If namespace has a wildcard, filter results against Corefile namespace list. // (Namespaces without a wildcard were filtered before the call to this function.) - if nsWildcard && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(svc.Namespace, k.Namespaces)) { + if wildcard(r.namespace) && !k.namespaceExposed(svc.Namespace) { continue } s := kService{name: svc.Name, namespace: svc.Namespace} @@ -460,7 +457,7 @@ func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) { if r.endpoint != "" && r.endpoint != ephostname { continue } - if !(match(r.port, p.Name, portWildcard) && match(r.protocol, string(p.Protocol), protocolWildcard)) { + if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) { continue } s.endpoints = append(s.endpoints, endpoint{addr: addr, port: p}) @@ -484,7 +481,7 @@ func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) { // ClusterIP service s.addr = svc.Spec.ClusterIP for _, p := range svc.Spec.Ports { - if !(match(r.port, p.Name, portWildcard) && match(r.protocol, string(p.Protocol), protocolWildcard)) { + if !(match(r.port, p.Name) && match(r.protocol, string(p.Protocol))) { continue } s.ports = append(s.ports, p) @@ -495,21 +492,30 @@ func (k *Kubernetes) findServices(r recordRequest) ([]kService, error) { return resultItems, nil } -func match(a, b string, wildcard bool) bool { - if wildcard { +// match checks if a and b are equal taking wildcards into account. +func match(a, b string) bool { + if wildcard(a) { + return true + } + if wildcard(b) { return true } return strings.EqualFold(a, b) } -// getServiceRecordForIP: Gets a service record with a cluster ip matching the ip argument +// wildcard checks whether s contains a wildcard value defined as "*" or "any". +func wildcard(s string) bool { + return s == "*" || s == "any" +} + +// serviceRecordForIP gets a service record with a cluster ip matching the ip argument // If a service cluster ip does not match, it checks all endpoints -func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service { +func (k *Kubernetes) serviceRecordForIP(ip, name string) []msg.Service { // First check services with cluster ips svcList := k.APIConn.ServiceList() for _, service := range svcList { - if (len(k.Namespaces) > 0) && !dnsstrings.StringInSlice(service.Namespace, k.Namespaces) { + if (len(k.Namespaces) > 0) && !k.namespaceExposed(service.Namespace) { continue } if service.Spec.ClusterIP == ip { @@ -520,7 +526,7 @@ func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service { // If no cluster ips match, search endpoints epList := k.APIConn.EndpointsList() for _, ep := range epList.Items { - if (len(k.Namespaces) > 0) && !dnsstrings.StringInSlice(ep.ObjectMeta.Namespace, k.Namespaces) { + if (len(k.Namespaces) > 0) && !k.namespaceExposed(ep.ObjectMeta.Namespace) { continue } for _, eps := range ep.Subsets { @@ -537,20 +543,13 @@ func (k *Kubernetes) getServiceRecordForIP(ip, name string) []msg.Service { // namespaceExposed returns true when the namespace is exposed. func (k *Kubernetes) namespaceExposed(namespace string) bool { - // Abort if the namespace does not contain a wildcard, and namespace is - // not published per CoreFile Case where namespace contains a wildcard - // is handled in k.get(...) method. - if (!wildcard(namespace)) && (len(k.Namespaces) > 0) && (!dnsstrings.StringInSlice(namespace, k.Namespaces)) { + _, ok := k.Namespaces[namespace] + if len(k.Namespaces) > 0 && !ok { return false } return true } -// wildcard checks whether s contains a wildcard value -func wildcard(s string) bool { - return (s == "*" || s == "any") -} - const ( // Svc is the DNS schema for kubernetes services Svc = "svc" diff --git a/middleware/kubernetes/parse.go b/middleware/kubernetes/parse.go index 700dc654a..c0b5de680 100644 --- a/middleware/kubernetes/parse.go +++ b/middleware/kubernetes/parse.go @@ -13,80 +13,82 @@ type recordRequest struct { 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 + protocol string + endpoint string + // The servicename used in Kubernetes. + service string + // The namespace used in Kubernetes. 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. + // A each name can be for a pod or a service, here we track what we've seen, either "pod" or "service". podOrSvc string } -// parseRequest parses the qname to find all the elements we need for querying k8s. +// parseRequest parses the qname to find all the elements we need for querying k8s. Anything +// that is not parsed will have the wildcard "*" value. Potential underscores are stripped +// from _port and _protocol. func (k *Kubernetes) parseRequest(state request.Request) (r recordRequest, err error) { // 3 Possible cases: - // o SRV Request: _port._protocol.service.namespace.type.zone - // o A Request (endpoint): endpoint.service.namespace.type.zone - // o A Request (service): service.namespace.type.zone + // o SRV Request: _port._protocol.service.namespace.pod|svc.zone + // o A Request (endpoint): endpoint.service.namespace.pod|svc.zone + // o A Request (service): service.namespace.pod|svc.zone + // // Federations are handled in the federation middleware. base, _ := dnsutil.TrimZone(state.Name(), state.Zone) segs := dns.SplitDomainName(base) - offset := 0 - if state.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 (state.QType() == dns.TypeA || state.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 - } + r.port = "*" + r.protocol = "*" + r.service = "*" + r.endpoint = "" // TODO(miek): dangerous; should just work with "*", but "" is checked in k.get() + r.namespace = "*" - if len(segs) == (offset + 3) { - r.service = segs[offset] - r.namespace = segs[offset+1] - r.podOrSvc = segs[offset+2] + // start at the right and fill out recordRequest with the bits we find, so we look for + // pod|svc.namespace.service and then either + // * endpoint + // *_protocol._port + last := len(segs) - 1 + r.podOrSvc = segs[last] + if r.podOrSvc != Pod && r.podOrSvc != Svc { + return r, errInvalidRequest + } + last-- + if last < 0 { return r, nil } - return r, errInvalidRequest + r.namespace = segs[last] + last-- + if last < 0 { + return r, nil + } + + r.service = segs[last] + last-- + if last < 0 { + return r, nil + } + + if segs[last][0] == '_' { + r.protocol = segs[last][1:] + } else { + r.endpoint = segs[last] + } + last-- + if last < 0 { + return r, nil + } + + if segs[last][0] == '_' { + r.port = segs[last][1:] + } + + if last > 0 { // Too long, so NXDOMAIN these. + return r, errInvalidRequest + + } + return r, nil } // String return a string representation of r, it just returns all fields concatenated with dots. diff --git a/middleware/kubernetes/parse_test.go b/middleware/kubernetes/parse_test.go index daf884b30..720b831b2 100644 --- a/middleware/kubernetes/parse_test.go +++ b/middleware/kubernetes/parse_test.go @@ -24,18 +24,12 @@ func TestParseRequest(t *testing.T) { { // wildcard acceptance "*.any.*.any.svc.inter.webs.test.", dns.TypeSRV, - "*.any..*.any.svc", + "*.*.any.*.any.svc", }, { // A request of endpoint "1-2-3-4.webs.mynamespace.svc.inter.webs.test.", dns.TypeA, - "..1-2-3-4.webs.mynamespace.svc", - }, - { - - // 3 segments - "webs.mynamespace.svc.inter.webs.test.", dns.TypeSRV, - "*...webs.mynamespace.svc", + "*.*.1-2-3-4.webs.mynamespace.svc", }, } for i, tc := range tests { @@ -57,21 +51,18 @@ func TestParseRequest(t *testing.T) { func TestParseInvalidRequest(t *testing.T) { k := New([]string{zone}) - invalid := map[string]uint16{ - "_http._tcp.webs.mynamespace.svc.inter.webs.test.": dns.TypeA, // A requests cannot have port or protocol - "_http._pcp.webs.mynamespace.svc.inter.webs.test.": dns.TypeSRV, // SRV protocol must be tcp or udp - "_http._tcp.ep.webs.ns.svc.inter.webs.test.": dns.TypeSRV, // SRV requests cannot have an endpoint - "_*._*.webs.mynamespace.svc.inter.webs.test.": dns.TypeSRV, // SRV request with invalid wildcards - + invalid := []string{ + "webs.mynamespace.pood.inter.webs.test.", // Request must be for pod or svc subdomain. + "too.long.for.what.I.am.trying.to.do.inter.webs.tests.", // Too long. } - for query, qtype := range invalid { + for i, query := range invalid { m := new(dns.Msg) - m.SetQuestion(query, qtype) + m.SetQuestion(query, dns.TypeA) state := request.Request{Zone: zone, Req: m} if _, e := k.parseRequest(state); e == nil { - t.Errorf("Expected error from %s:%d, got none", query, qtype) + t.Errorf("Test %d: expected error from %s, got none", i, query) } } } diff --git a/middleware/kubernetes/reverse.go b/middleware/kubernetes/reverse.go index b7392343d..62b3a7043 100644 --- a/middleware/kubernetes/reverse.go +++ b/middleware/kubernetes/reverse.go @@ -15,6 +15,6 @@ func (k *Kubernetes) Reverse(state request.Request, exact bool, opt middleware.O return nil, nil, nil } - records := k.getServiceRecordForIP(ip, state.Name()) + records := k.serviceRecordForIP(ip, state.Name()) return records, nil, nil } diff --git a/middleware/kubernetes/setup.go b/middleware/kubernetes/setup.go index 123aa8001..3403df90e 100644 --- a/middleware/kubernetes/setup.go +++ b/middleware/kubernetes/setup.go @@ -62,13 +62,9 @@ func setup(c *caddy.Controller) error { } func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { - k8s := &Kubernetes{ - ResyncPeriod: defaultResyncPeriod, - interfaceAddrsFunc: localPodIP, - PodMode: PodModeDisabled, - Proxy: proxy.Proxy{}, - autoPathSearch: searchFromResolvConf(), - } + k8s := New([]string{""}) + k8s.interfaceAddrsFunc = localPodIP + k8s.autoPathSearch = searchFromResolvConf() for c.Next() { zones := c.RemainingArgs() @@ -115,7 +111,9 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { case "namespaces": args := c.RemainingArgs() if len(args) > 0 { - k8s.Namespaces = append(k8s.Namespaces, args...) + for _, a := range args { + k8s.Namespaces[a] = true + } continue } return nil, c.ArgErr() diff --git a/middleware/pkg/strings/slice.go b/middleware/pkg/strings/slice.go deleted file mode 100644 index 3d4b1d481..000000000 --- a/middleware/pkg/strings/slice.go +++ /dev/null @@ -1,11 +0,0 @@ -package strings - -// StringInSlice check whether string a is a member of slice. -func StringInSlice(a string, slice []string) bool { - for _, b := range slice { - if b == a { - return true - } - } - return false -}