diff options
author | 2016-07-14 14:50:14 -0700 | |
---|---|---|
committer | 2016-07-14 23:50:14 +0200 | |
commit | 3f4ec783d218d10b244487d463de5bb31284e3cf (patch) | |
tree | e9214e93511b36039a54462aba214b5f82387f93 /middleware | |
parent | 319d30697af07616be596fd4918dce3ce4bcfc84 (diff) | |
download | coredns-3f4ec783d218d10b244487d463de5bb31284e3cf.tar.gz coredns-3f4ec783d218d10b244487d463de5bb31284e3cf.tar.zst coredns-3f4ec783d218d10b244487d463de5bb31284e3cf.zip |
Adding wildcard support (#190)
* Commenting out unused functions. TODO: remove when it is not needed
* Update README with namespace and template example
* Adding note about changing the record name format via a template
* Adding test scripts to automate k8s startup
* Automating k8s namespace creation
* Adding automation to start 4 k8s services
* Updating documentation for k8s tests
* Avoid downloading kubectl if already exists
* Adding debug statement when namespace is not exposed.
* Adding basic kubernetes integration tests
* Makefile now contains a "testk8s" target. This target requires k8s to
be running.
* Adding test/kubernetes_test.go file with a couple of basic A record
tests.
* Updating k8s integration tests to only run k8s integration tests
* Adding support for namespace wildcards
* Refactoring to move filtering logic to kubernetes.go file
* go fmt fixes
* Adding wildcard support for namespaces and service names
* Kubernetes integration tests updated for A records.
* Expanded record name assembly for answer section not yet implemented.
* Refactoring to focus k8sclient code just on accessing k8s API.
Filtering now handled in kubernetes.go
* Adding wildcard test cases
* Adding skydns startup script. (To allow side by side testing of wildcards.)
* Commenting out record name assmebly based on NameTemplate. Need to improve template before this makes sense.
* Adding basic SRV integration tests
* Need to add verification for additional answer section
* Fixing comments and formatting
* Moving wildcard constants to vars
* Travis test execution appears to be failing on access to these
constants
* Fixing access to util package
* Trying to work around Travis test bug
* Reverting to access kubernetes/util as "util"
Travis breakage is due to "Infoblox-CTO" in src path
Diffstat (limited to 'middleware')
-rw-r--r-- | middleware/kubernetes/README.md | 10 | ||||
-rw-r--r-- | middleware/kubernetes/handler.go | 1 | ||||
-rw-r--r-- | middleware/kubernetes/k8sclient/k8sclient.go | 24 | ||||
-rw-r--r-- | middleware/kubernetes/kubernetes.go | 105 | ||||
-rwxr-xr-x | middleware/kubernetes/test/00_run_k8s.sh | 39 | ||||
-rwxr-xr-x | middleware/kubernetes/test/10_setup_kubectl.sh | 18 | ||||
-rwxr-xr-x | middleware/kubernetes/test/15_run_skydns.sh | 39 | ||||
-rwxr-xr-x | middleware/kubernetes/test/20_setup_k8s_services.sh | 80 | ||||
-rw-r--r-- | middleware/kubernetes/test/README.md | 35 | ||||
-rwxr-xr-x | middleware/kubernetes/test/kill_all_containers.sh | 5 | ||||
-rw-r--r-- | middleware/kubernetes/util/util.go | 14 | ||||
-rw-r--r-- | middleware/kubernetes/util/util_test.go | 22 |
12 files changed, 332 insertions, 60 deletions
diff --git a/middleware/kubernetes/README.md b/middleware/kubernetes/README.md index c5ba32f72..2f2f7341d 100644 --- a/middleware/kubernetes/README.md +++ b/middleware/kubernetes/README.md @@ -7,6 +7,7 @@ are constructed as "myservice.mynamespace.coredns.local" where: * "mynamespace" is the k8s namespace for the service, and * "coredns.local" is the zone configured for `kubernetes`. +The record name format can be changed by specifying a name template in the Corefile. ## Syntax @@ -36,6 +37,10 @@ This is the default kubernetes setup, with everything specified in full: kubernetes coredns.local { # Use url for k8s API endpoint endpoint http://localhost:8080 + # Assemble k8s record names with the template + template {service}.{namespace}.{zone} + # Only expose the k8s namespace "demo" + namespaces demo } # cache 160 coredns.local } @@ -247,7 +252,7 @@ return the IP addresses for all services with "nginx" in the service name. TBD: * How does this relate the the k8s load-balancer configuration? -* Do wildcards search across namespaces? +* Do wildcards search across namespaces? (Yes) * Initial implementation assumes that a namespace maps to the first DNS label below the zone managed by the kubernetes middleware. This assumption may need to be revised. @@ -344,4 +349,5 @@ TBD: and A-record queries. Automate testing with cache in place. * Automate CoreDNS performance tests. Initially for zone files, and for pre-loaded k8s API cache. - * Automate integration testing with kubernetes. + * Automate integration testing with kubernetes. (k8s launch and service start-up + automation is in middleware/kubernetes/tests) diff --git a/middleware/kubernetes/handler.go b/middleware/kubernetes/handler.go index 168b65508..49d1c1573 100644 --- a/middleware/kubernetes/handler.go +++ b/middleware/kubernetes/handler.go @@ -10,7 +10,6 @@ import ( ) func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { - fmt.Printf("[debug] here entering ServeDNS: ctx:%v dnsmsg:%v\n", ctx, r) state := middleware.State{W: w, Req: r} diff --git a/middleware/kubernetes/k8sclient/k8sclient.go b/middleware/kubernetes/k8sclient/k8sclient.go index 95300f3b9..cb1d0fe4a 100644 --- a/middleware/kubernetes/k8sclient/k8sclient.go +++ b/middleware/kubernetes/k8sclient/k8sclient.go @@ -117,30 +117,6 @@ func (c *K8sConnector) GetServicesByNamespace() (map[string][]ServiceItem, error return items, nil } -// GetServiceItemsInNamespace returns the ServiceItems that match -// servicename in the namespace -func (c *K8sConnector) GetServiceItemsInNamespace(namespace string, servicename string) ([]*ServiceItem, error) { - - itemMap, err := c.GetServicesByNamespace() - - if err != nil { - fmt.Printf("[ERROR] Getting service list produced error: %v", err) - return nil, err - } - - // TODO: Handle case where namespace == nil - - var serviceItems []*ServiceItem - - for _, x := range itemMap[namespace] { - if x.Metadata.Name == servicename { - serviceItems = append(serviceItems, &x) - } - } - - return serviceItems, nil -} - func NewK8sConnector(baseURL string) *K8sConnector { k := new(K8sConnector) diff --git a/middleware/kubernetes/kubernetes.go b/middleware/kubernetes/kubernetes.go index d6d93f809..f9a0712fe 100644 --- a/middleware/kubernetes/kubernetes.go +++ b/middleware/kubernetes/kubernetes.go @@ -62,7 +62,7 @@ func (g Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { typeName string ) - fmt.Println("enter Records('", name, "', ", exact, ")") + fmt.Println("[debug] enter Records('", name, "', ", exact, ")") zone, serviceSegments := g.getZoneForName(name) /* @@ -83,6 +83,18 @@ func (g Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { serviceName = g.NameTemplate.GetServiceFromSegmentArray(serviceSegments) typeName = g.NameTemplate.GetTypeFromSegmentArray(serviceSegments) + if namespace == "" { + err := errors.New("Parsing query string did not produce a namespace value. Assuming wildcard namespace.") + fmt.Printf("[WARN] %v\n", err) + namespace = util.WildcardStar + } + + if serviceName == "" { + err := errors.New("Parsing query string did not produce a serviceName value. Assuming wildcard serviceName.") + fmt.Printf("[WARN] %v\n", err) + serviceName = util.WildcardStar + } + fmt.Println("[debug] exact: ", exact) fmt.Println("[debug] zone: ", zone) fmt.Println("[debug] servicename: ", serviceName) @@ -90,21 +102,18 @@ func (g Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { fmt.Println("[debug] typeName: ", typeName) fmt.Println("[debug] APIconn: ", g.APIConn) - // TODO: Implement wildcard support to allow blank namespace value - if namespace == "" { - err := errors.New("Parsing query string did not produce a namespace value") - fmt.Printf("[ERROR] %v\n", err) - return nil, err - } + nsWildcard := util.SymbolContainsWildcard(namespace) + serviceWildcard := util.SymbolContainsWildcard(serviceName) - // Abort if the namespace is not published per CoreFile - if g.Namespaces != nil && !util.StringInSlice(namespace, *g.Namespaces) { + // 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 Get(...) method. + if (!nsWildcard) && (g.Namespaces != nil && !util.StringInSlice(namespace, *g.Namespaces)) { + fmt.Printf("[debug] Namespace '%v' is not published by Corefile\n", namespace) return nil, nil } - k8sItems, err := g.APIConn.GetServiceItemsInNamespace(namespace, serviceName) + k8sItems, err := g.Get(namespace, nsWildcard, serviceName, serviceWildcard) fmt.Println("[debug] k8s items:", k8sItems) - if err != nil { fmt.Printf("[ERROR] Got error while looking up ServiceItems. Error is: %v\n", err) return nil, err @@ -114,29 +123,27 @@ func (g Kubernetes) Records(name string, exact bool) ([]msg.Service, error) { return nil, nil } - // test := g.NameTemplate.GetRecordNameFromNameValues(nametemplate.NameValues{ServiceName: serviceName, TypeName: typeName, Namespace: namespace, Zone: zone}) - // fmt.Printf("[debug] got recordname %v\n", test) - - records := g.getRecordsForServiceItems(k8sItems, name) - + records := g.getRecordsForServiceItems(k8sItems, nametemplate.NameValues{TypeName: typeName, ServiceName: serviceName, Namespace: namespace, Zone: zone}) return records, nil } // TODO: assemble name from parts found in k8s data based on name template rather than reusing query string -func (g Kubernetes) getRecordsForServiceItems(serviceItems []*k8sc.ServiceItem, name string) []msg.Service { +func (g Kubernetes) getRecordsForServiceItems(serviceItems []k8sc.ServiceItem, values nametemplate.NameValues) []msg.Service { var records []msg.Service for _, item := range serviceItems { - fmt.Println("[debug] clusterIP:", item.Spec.ClusterIP) - for _, p := range item.Spec.Ports { - fmt.Println("[debug] port:", p.Port) - } - clusterIP := item.Spec.ClusterIP + fmt.Println("[debug] clusterIP:", clusterIP) + + // Create records by constructing record name from template... + //values.Namespace = item.Metadata.Namespace + //values.ServiceName = item.Metadata.Name + //s := msg.Service{Host: g.NameTemplate.GetRecordNameFromNameValues(values)} + //records = append(records, s) - s := msg.Service{Host: name} - records = append(records, s) + // Create records for each exposed port... for _, p := range item.Spec.Ports { + fmt.Println("[debug] port:", p.Port) s := msg.Service{Host: clusterIP, Port: p.Port} records = append(records, s) } @@ -146,17 +153,50 @@ func (g Kubernetes) getRecordsForServiceItems(serviceItems []*k8sc.ServiceItem, return records } -/* // Get performs the call to the Kubernetes http API. -func (g Kubernetes) Get(path string, recursive bool) (bool, error) { +func (g Kubernetes) Get(namespace string, nsWildcard bool, servicename string, serviceWildcard bool) ([]k8sc.ServiceItem, error) { + serviceList, err := g.APIConn.GetServiceList() - fmt.Println("[debug] in Get path: ", path) - fmt.Println("[debug] in Get recursive: ", recursive) + if err != nil { + fmt.Printf("[ERROR] Getting service list produced error: %v", err) + return nil, err + } - return false, nil + var resultItems []k8sc.ServiceItem + + for _, item := range serviceList.Items { + if symbolMatches(namespace, item.Metadata.Namespace, nsWildcard) && symbolMatches(servicename, item.Metadata.Name, serviceWildcard) { + // 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 && (g.Namespaces != nil && !util.StringInSlice(item.Metadata.Namespace, *g.Namespaces)) { + fmt.Printf("[debug] Namespace '%v' is not published by Corefile\n", item.Metadata.Namespace) + continue + } + resultItems = append(resultItems, item) + } + } + + return resultItems, nil } -*/ +func symbolMatches(queryString string, candidateString string, wildcard bool) bool { + result := false + switch { + case !wildcard: + result = (queryString == candidateString) + case queryString == util.WildcardStar: + result = true + case queryString == util.WildcardAny: + result = true + } + return result +} + +// TODO: Remove these unused functions. One is related to Ttl calculation +// Implement Ttl and priority calculation based on service count before +// removing this code. +/* +// splitDNSName separates the name into DNS segments and reverses the segments. func (g Kubernetes) splitDNSName(name string) []string { l := dns.SplitDomainName(name) @@ -166,16 +206,15 @@ func (g Kubernetes) splitDNSName(name string) []string { return l } - +*/ // skydns/local/skydns/east/staging/web // skydns/local/skydns/west/production/web // // skydns/local/skydns/*/*/web // skydns/local/skydns/*/web - +/* // loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname // will be match against any wildcards when star is true. -/* func (g Kubernetes) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) { if bx == nil { bx = make(map[msg.Service]bool) diff --git a/middleware/kubernetes/test/00_run_k8s.sh b/middleware/kubernetes/test/00_run_k8s.sh new file mode 100755 index 000000000..3753cbd8e --- /dev/null +++ b/middleware/kubernetes/test/00_run_k8s.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Based on instructions at: http://kubernetes.io/docs/getting-started-guides/docker/ + +#K8S_VERSION=$(curl -sS https://storage.googleapis.com/kubernetes-release/release/latest.txt) +K8S_VERSION="v1.2.4" + +ARCH="amd64" + + +export K8S_VERSION +export ARCH + +#RUN_SKYDNS="yes" +RUN_SKYDNS="no" + +if [ "${RUN_SKYDNS}" = "yes" ]; then + DNS_ARGUMENTS="--cluster-dns=10.0.0.10 --cluster-domain=cluster.local" +else + DNS_ARGUMENTS="" +fi + +docker run -d \ + --volume=/:/rootfs:ro \ + --volume=/sys:/sys:ro \ + --volume=/var/lib/docker/:/var/lib/docker:rw \ + --volume=/var/lib/kubelet/:/var/lib/kubelet:rw \ + --volume=/var/run:/var/run:rw \ + --net=host \ + --pid=host \ + --privileged \ + gcr.io/google_containers/hyperkube-${ARCH}:${K8S_VERSION} \ + /hyperkube kubelet \ + --containerized \ + --hostname-override=127.0.0.1 \ + --api-servers=http://localhost:8080 \ + --config=/etc/kubernetes/manifests \ + ${DNS_ARGUMENTS} \ + --allow-privileged --v=2 diff --git a/middleware/kubernetes/test/10_setup_kubectl.sh b/middleware/kubernetes/test/10_setup_kubectl.sh new file mode 100755 index 000000000..c47b779a3 --- /dev/null +++ b/middleware/kubernetes/test/10_setup_kubectl.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +PWD=`pwd` +BASEDIR=`realpath $(dirname ${0})` + +cd ${BASEDIR} +if [ ! -e kubectl ]; then + curl -O http://storage.googleapis.com/kubernetes-release/release/v1.2.4/bin/linux/amd64/kubectl + chmod u+x kubectl +fi + +${BASEDIR}/kubectl config set-cluster test-doc --server=http://localhost:8080 +${BASEDIR}/kubectl config set-context test-doc --cluster=test-doc +${BASEDIR}/kubectl config use-context test-doc + +cd ${PWD} + +alias kubctl="${BASEDIR}/kubectl" diff --git a/middleware/kubernetes/test/15_run_skydns.sh b/middleware/kubernetes/test/15_run_skydns.sh new file mode 100755 index 000000000..b88cf8f9b --- /dev/null +++ b/middleware/kubernetes/test/15_run_skydns.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# Running skydns based on instructions at: https://testdatamanagement.wordpress.com/2015/09/01/running-kubernetes-in-docker-with-dns-on-a-single-node/ + +KUBECTL='./kubectl' + +#RUN_SKYDNS="yes" +RUN_SKYDNS="no" + +wait_until_k8s_ready() { + # Wait until kubernetes is up and fully responsive + while : + do + ${KUBECTL} get nodes 2>/dev/null | grep -q '127.0.0.1' + if [ "${?}" = "0" ]; then + break + else + echo "sleeping for 5 seconds" + sleep 5 + fi + done + echo "kubernetes nodes:" + ${KUBECTL} get nodes +} + + +if [ "${RUN_SKYDNS}" = "yes" ]; then + wait_until_k8s_ready + + echo "Launch kube2sky..." + docker run -d --net=host gcr.io/google_containers/kube2sky:1.11 --kube_master_url=http://127.0.0.1:8080 --domain=cluster.local + + echo "" + + echo "Launch SkyDNS..." + docker run -d --net=host gcr.io/google_containers/skydns:2015-03-11-001 --machines=http://localhost:4001 --addr=0.0.0.0:53 --domain=cluster.local +else + true +fi diff --git a/middleware/kubernetes/test/20_setup_k8s_services.sh b/middleware/kubernetes/test/20_setup_k8s_services.sh new file mode 100755 index 000000000..0d067cf26 --- /dev/null +++ b/middleware/kubernetes/test/20_setup_k8s_services.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +KUBECTL='./kubectl' + +wait_until_k8s_ready() { + # Wait until kubernetes is up and fully responsive + while : + do + ${KUBECTL} get nodes 2>/dev/null | grep -q '127.0.0.1' + if [ "${?}" = "0" ]; then + break + else + echo "sleeping for 5 seconds" + sleep 5 + fi + done + echo "kubernetes nodes:" + ${KUBECTL} get nodes +} + +create_namespaces() { + for n in ${NAMESPACES}; + do + echo "Creating namespace: ${n}" + ${KUBECTL} get namespaces --no-headers 2>/dev/null | grep -q ${n} + if [ "${?}" != "0" ]; then + ${KUBECTL} create namespace ${n} + fi + done + + echo "kubernetes namespaces:" + ${KUBECTL} get namespaces +} + +# run_and_expose_service <servicename> <namespace> <image> <port> +run_and_expose_service() { + + if [ "${#}" != "4" ]; then + return -1 + fi + + service="${1}" + namespace="${2}" + image="${3}" + port="${4}" + + echo " starting service '${service}' in namespace '${namespace}" + + ${KUBECTL} get deployment --namespace=${namespace} --no-headers 2>/dev/null | grep -q ${service} + if [ "${?}" != "0" ]; then + ${KUBECTL} run ${service} --namespace=${namespace} --image=${image} + else + echo "warn: service '${service}' already running in namespace '${namespace}'" + fi + + ${KUBECTL} get service --namespace=${namespace} --no-headers 2>/dev/null | grep -q ${service} + if [ "${?}" != "0" ]; then + ${KUBECTL} expose deployment ${service} --namespace=${namespace} --port=${port} + else + echo "warn: service '${service}' already exposed in namespace '${namespace}'" + fi +} + + +wait_until_k8s_ready + +NAMESPACES="demo test" +create_namespaces + +echo "" +echo "Starting services:" + +run_and_expose_service mynginx demo nginx 80 +run_and_expose_service webserver demo nginx 80 +run_and_expose_service mynginx test nginx 80 +run_and_expose_service webserver test nginx 80 + +echo "" +echo "Services exposed:" +${KUBECTL} get services --all-namespaces diff --git a/middleware/kubernetes/test/README.md b/middleware/kubernetes/test/README.md new file mode 100644 index 000000000..eea1bf7d3 --- /dev/null +++ b/middleware/kubernetes/test/README.md @@ -0,0 +1,35 @@ +## Test scripts to automate kubernetes startup + +Requirements: + docker + curl + +The scripts in this directory startup kubernetes with docker as the container runtime. +After starting kubernetes, a couple of kubernetes services are started to allow automatic +testing of CoreDNS with kubernetes. + +To use, run the scripts as: + +~~~ +$ ./00_run_k8s.sh && ./10_setup_kubectl.sh && ./20_setup_k8s_services.sh +~~~ + +After running the above scripts, kubernetes will be running on the localhost with the following services +exposed: + +~~ +NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE +default kubernetes 10.0.0.1 <none> 443/TCP 48m +demo mynginx 10.0.0.168 <none> 80/TCP 9m +demo webserver 10.0.0.28 <none> 80/TCP 2m +test mynginx 10.0.0.4 <none> 80/TCP 2m +test webserver 10.0.0.39 <none> 80/TCP 2m +~~ + + +Kubernetes and all running containers can be uncerimoniously stopped by +running the `kill_all_containers.sh` script. + +~~~ +$ ./kill_all_containers.sh +~~~ diff --git a/middleware/kubernetes/test/kill_all_containers.sh b/middleware/kubernetes/test/kill_all_containers.sh new file mode 100755 index 000000000..aa7f4255d --- /dev/null +++ b/middleware/kubernetes/test/kill_all_containers.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +docker rm -f $(docker ps -a -q) +sleep 1 +docker rm -f $(docker ps -a -q) diff --git a/middleware/kubernetes/util/util.go b/middleware/kubernetes/util/util.go index 7fc03ffc1..259eaf596 100644 --- a/middleware/kubernetes/util/util.go +++ b/middleware/kubernetes/util/util.go @@ -1,6 +1,10 @@ // Package kubernetes/util provides helper functions for the kubernetes middleware package util +import ( + "strings" +) + // StringInSlice check whether string a is a member of slice. func StringInSlice(a string, slice []string) bool { for _, b := range slice { @@ -10,3 +14,13 @@ func StringInSlice(a string, slice []string) bool { } return false } + +// SymbolContainsWildcard checks whether symbol contains a wildcard value +func SymbolContainsWildcard(symbol string) bool { + return (strings.Contains(symbol, WildcardStar) || (symbol == WildcardAny)) +} + +const ( + WildcardStar = "*" + WildcardAny = "any" +) diff --git a/middleware/kubernetes/util/util_test.go b/middleware/kubernetes/util/util_test.go index b53b9f3f6..4af64ea50 100644 --- a/middleware/kubernetes/util/util_test.go +++ b/middleware/kubernetes/util/util_test.go @@ -31,3 +31,25 @@ func TestStringInSlice(t *testing.T) { } } } + +// Test data for TestSymbolContainsWildcard cases. +var testdataSymbolContainsWildcard = []struct { + Symbol string + ExpectedResult bool +}{ + {"mynamespace", false}, + {"*", true}, + {"any", true}, + {"my*space", true}, + {"*space", true}, + {"myname*", true}, +} + +func TestSymbolContainsWildcard(t *testing.T) { + for _, example := range testdataSymbolContainsWildcard { + actualResult := SymbolContainsWildcard(example.Symbol) + if actualResult != example.ExpectedResult { + t.Errorf("Expected SymbolContainsWildcard result '%v' for example string='%v'. Instead got result '%v'.", example.ExpectedResult, example.Symbol, actualResult) + } + } +} |