diff --git a/middleware/kubernetes/README.md b/middleware/kubernetes/README.md index e2e1fa09a..54dd98e57 100644 --- a/middleware/kubernetes/README.md +++ b/middleware/kubernetes/README.md @@ -43,21 +43,36 @@ This is the default kubernetes setup, with everything specified in full: # Kubernetes data API resync period # Example values: 60s, 5m, 1h resyncperiod 5m + # Use url for k8s API endpoint endpoint https://k8sendpoint:8080 + # The tls cert, key and the CA cert filenames tls cert key cacert + # Assemble k8s record names with the template template {service}.{namespace}.{type}.{zone} + # Only expose the k8s namespace "demo" namespaces demo + # Only expose the records for kubernetes objects # that match this label selector. The label # selector syntax is described in the kubernetes # API documentation: http://kubernetes.io/docs/user-guide/labels/ # Example selector below only exposes objects tagged as # "application=nginx" in the staging or qa environments. - labels environment in (staging, qa),application=nginx + #labels environment in (staging, qa),application=nginx + + # The mode of responding to pod A record requests. + # e.g 1-2-3-4.ns.pod.zone. This option is provided to allow use of + # SSL certs when connecting directly to pods. + # Valid values: disabled, verified, insecure + # disabled: default. ignore pod requests, always returning NXDOMAIN + # insecure: Always return an A record with IP from request (without + # checking k8s). This option is is vulnerable to abuse if + # used maliciously in conjuction with wildcard SSL certs. + pods disabled } # Perform DNS response caching for the coredns.local zone # Cache timeout is specified by an integer in seconds @@ -72,6 +87,7 @@ Defaults: * The `labels` keyword is only used when filtering results based on kubernetes label selector syntax is required. The label selector syntax is described in the kubernetes API documentation at: http://kubernetes.io/docs/user-guide/labels/ +* If the `pods` keyword is omitted, all pod type requests will result in NXDOMAIN ### Template Syntax Record name templates can be constructed using the symbolic elements: diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index 68086702e..85e4940ac 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -42,8 +42,14 @@ type Kubernetes struct { Namespaces []string LabelSelector *unversionedapi.LabelSelector Selector *labels.Selector + PodMode string } +const ( + PodModeDisabled = "disabled" // default. pod requests are ignored + PodModeInsecure = "insecure" // ALL pod requests are answered without verfying they exist +) + type endpoint struct { addr api.EndpointAddress port api.EndpointPort @@ -57,6 +63,12 @@ type service struct { endpoints []endpoint } +type pod struct { + name string + namespace string + addr string +} + var errNoItems = errors.New("no items found") var errNsNotExposed = errors.New("namespace is not exposed") var errInvalidRequest = errors.New("invalid query name") @@ -221,12 +233,9 @@ func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { return nil, errNoItems } - // TODO: Implementation above globbed together segments for the serviceName if - // multiple segments remained. Determine how to do similar globbing using - // the template-based implementation. - namespace = k.NameTemplate.NamespaceFromSegmentArray(serviceSegments) - serviceName = k.NameTemplate.ServiceFromSegmentArray(serviceSegments) - typeName = k.NameTemplate.TypeFromSegmentArray(serviceSegments) + serviceName = serviceSegments[0] + namespace = serviceSegments[1] + typeName = serviceSegments[2] if namespace == "" { err := errors.New("Parsing query string did not produce a namespace value. Assuming wildcard namespace.") @@ -246,16 +255,16 @@ func (k *Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { return nil, errNsNotExposed } - k8sItems, err := k.Get(namespace, serviceName, endpointname, port, protocol, typeName) + services, pods, err := k.Get(namespace, serviceName, endpointname, port, protocol, typeName) if err != nil { return nil, err } - if len(k8sItems) == 0 { + if len(services) == 0 && len(pods) == 0 { // Did not find item in k8s return nil, errNoItems } - records := k.getRecordsForServiceItems(k8sItems, zone) + records := k.getRecordsForK8sItems(services, pods, zone) return records, nil } @@ -272,10 +281,10 @@ func endpointHostname(addr api.EndpointAddress) string { return "" } -func (k *Kubernetes) getRecordsForServiceItems(serviceItems []service, zone string) []msg.Service { +func (k *Kubernetes) getRecordsForK8sItems(services []service, pods []pod, zone string) []msg.Service { var records []msg.Service - for _, svc := range serviceItems { + for _, svc := range services { key := svc.name + "." + svc.namespace + ".svc." + zone @@ -299,20 +308,61 @@ func (k *Kubernetes) getRecordsForServiceItems(serviceItems []service, zone stri } } + for _, p := range pods { + key := p.name + "." + p.namespace + ".pod." + zone + s := msg.Service{ + Key: msg.Path(strings.ToLower(key), "coredns"), + Host: p.addr, + } + records = append(records, s) + } + return records } -// Get performs the call to the Kubernetes http API. -func (k *Kubernetes) Get(namespace, servicename, endpointname, port, protocol, typeName string) (services []service, err error) { +// Get retrieves matching data from the cache. +func (k *Kubernetes) Get(namespace, servicename, endpointname, port, protocol, typeName string) (services []service, pods []pod, err error) { switch { case typeName == "pod": - return nil, fmt.Errorf("%v not implemented", typeName) + pods, err = k.findPods(namespace, servicename) + return nil, pods, err default: - return k.getServices(namespace, servicename, endpointname, port, protocol) + services, err = k.findServices(namespace, servicename, endpointname, port, protocol) + return services, nil, err } } -func (k *Kubernetes) getServices(namespace, servicename, endpointname, port, protocol string) ([]service, error) { +func ipFromPodName(podname string) string { + if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") { + return strings.Replace(podname, "-", ".", -1) + } + return strings.Replace(podname, "-", ":", -1) +} + +func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) { + if k.PodMode == PodModeDisabled { + return pods, nil + } + + var ip string + if strings.Count(podname, "-") == 3 && !strings.Contains(podname, "--") { + ip = strings.Replace(podname, "-", ".", -1) + } else { + ip = strings.Replace(podname, "-", ":", -1) + } + + if k.PodMode == PodModeInsecure { + s := pod{name: podname, namespace: namespace, addr: ip} + pods = append(pods, s) + return pods, nil + } + + // TODO: implement cache verified pod responses + return pods, nil + +} + +func (k *Kubernetes) findServices(namespace, servicename, endpointname, port, protocol string) ([]service, error) { serviceList := k.APIConn.ServiceList() var resultItems []service diff --git a/middleware/kubernetes/setup.go b/middleware/kubernetes/setup.go index d2b34b054..8c733a2df 100644 --- a/middleware/kubernetes/setup.go +++ b/middleware/kubernetes/setup.go @@ -54,6 +54,7 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { k8s := &Kubernetes{ResyncPeriod: defaultResyncPeriod} k8s.NameTemplate = new(nametemplate.Template) k8s.NameTemplate.SetTemplate(defaultNameTemplate) + k8s.PodMode = PodModeDisabled for c.Next() { if c.Val() == "kubernetes" { @@ -86,6 +87,19 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { for c.NextBlock() { switch c.Val() { + case "pods": + args := c.RemainingArgs() + if len(args) == 1 { + switch args[0] { + case PodModeDisabled, PodModeInsecure: + k8s.PodMode = args[0] + default: + return nil, errors.New("pods must be one of: disabled, insecure") + } + continue + } + return nil, c.ArgErr() + case "template": args := c.RemainingArgs() if len(args) > 0 { @@ -152,4 +166,5 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { const ( defaultNameTemplate = "{service}.{namespace}.{type}.{zone}" defaultResyncPeriod = 5 * time.Minute + defaultPodMode = PodModeDisabled ) diff --git a/test/kubernetes_test.go b/test/kubernetes_test.go index 1d5df37a3..02f731475 100644 --- a/test/kubernetes_test.go +++ b/test/kubernetes_test.go @@ -194,6 +194,18 @@ var dnsTestCases = []test.Case{ Rcode: dns.RcodeNameError, Answer: []dns.RR{}, }, + { + Qname: "10-20-0-101.test-1.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.A("10-20-0-101.test-1.pod.cluster.local. 0 IN A 10.20.0.101"), + }, + }, + { + Qname: "10-20-0-101.test-X.pod.cluster.local.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Answer: []dns.RR{}, + }, { Qname: "123.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, Rcode: dns.RcodeSuccess, @@ -238,6 +250,7 @@ func TestKubernetesIntegration(t *testing.T) { #tls admin.pem admin-key.pem ca.pem #tls k8s_auth/client2.crt k8s_auth/client2.key k8s_auth/ca2.crt namespaces test-1 + pods insecure } ` server, udp := createTestServer(t, corefile)