mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	middleware/kubernetes: Server side path lookups (#750)
* initial commit * add config options * add readme * rewording * revert unlreated change * normalize host domain path * add ndots opt, allow > 1 host domains, pull host domains from resolv.conf * implementing review feedback * update readme * use dns lib, config format, defaults * Correct autopath example.
This commit is contained in:
		
				
					committed by
					
						 John Belamaric
						John Belamaric
					
				
			
			
				
	
			
			
			
						parent
						
							817f3960b8
						
					
				
				
					commit
					edf71fb168
				
			| @@ -121,6 +121,58 @@ kubernetes coredns.local { | |||||||
| 	# Each line consists of the name of the federation, and the domain. | 	# Each line consists of the name of the federation, and the domain. | ||||||
| 	federation myfed foo.example.com | 	federation myfed foo.example.com | ||||||
| 	 | 	 | ||||||
|  | 	# autopath [NDOTS [RESPONSE [RESOLV-CONF]] | ||||||
|  | 	# | ||||||
|  | 	# Enables server side search path lookups for pods.  When enabled, coredns | ||||||
|  | 	# will identify search path queries from pods and perform the remaining | ||||||
|  | 	# lookups in the path on the pod's behalf.  The search path used mimics the | ||||||
|  | 	# resolv.conf search path deployed to pods. E.g. | ||||||
|  | 	# | ||||||
|  | 	#  search ns1.svc.cluster.local svc.cluster.local cluster.local foo.com | ||||||
|  | 	# | ||||||
|  | 	# If no domains in the path produce an answer, a lookup on the bare question | ||||||
|  | 	# will be attempted.	 | ||||||
|  | 	# | ||||||
|  | 	# A successful response will contain a question section with the original | ||||||
|  | 	# question, and an answer section containing the record for the question that | ||||||
|  | 	# actually had an answer.  This means that the question and answer will not | ||||||
|  | 	# match. For example: | ||||||
|  | 	# | ||||||
|  | 	#    # host -v -t a google.com | ||||||
|  | 	#    Trying "google.com.default.svc.cluster.local" | ||||||
|  | 	#    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50957 | ||||||
|  | 	#    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 | ||||||
|  | 	# | ||||||
|  | 	#    ;; QUESTION SECTION: | ||||||
|  | 	#    ;google.com.default.svc.cluster.local. IN A | ||||||
|  | 	# | ||||||
|  | 	#    ;; ANSWER SECTION: | ||||||
|  | 	#    google.com.		175	IN	A	216.58.194.206 | ||||||
|  | 	# | ||||||
|  | 	# | ||||||
|  | 	# NDOTS (default: 0) This provides an adjustable threshold to | ||||||
|  | 	# prevent server side lookups from triggering. If the number of dots before | ||||||
|  | 	# the first search domain is less than this number, then the search path will | ||||||
|  | 	# not executed on the server side. | ||||||
|  | 	# | ||||||
|  | 	# RESPONSE (default: SERVFAIL) RESPONSE can be either NXDOMAIN, SERVFAIL or | ||||||
|  | 	# NOERROR. This option causes coredns to return the given response instead of | ||||||
|  | 	# NXDOMAIN when the all searches in the path produce no results. Setting this | ||||||
|  | 	# to SERVFAIL or NOERROR should prevent the client from fruitlessly continuing | ||||||
|  | 	# the client side searches in the path after the server already checked them. | ||||||
|  | 	# | ||||||
|  | 	# RESOLV-CONF (default: /etc/resolv.conf) If specified, coredns uses this | ||||||
|  | 	# file to get the host's search domains. CoreDNS performs a lookup on these | ||||||
|  | 	# domains if the in-cluster search domains in the path fail to produce an | ||||||
|  | 	# answer. If not specified, the values will be read from the local resolv.conf | ||||||
|  | 	# file (i.e the resolv.conf file in the pod containing coredns). | ||||||
|  | 	# | ||||||
|  | 	# Enabling autopath causes coredns to use more memory since it needs to | ||||||
|  | 	# maintain a watch on all pods. If autopath and "pods verified" mode are | ||||||
|  | 	# both enabled, they will share the same watch. I.e. enabling both options | ||||||
|  | 	# should have an equivalent memory impact of just one. | ||||||
|  | 	autopath 0 SERVFAIL /etc/resolv.conf | ||||||
|  |  | ||||||
| 	# fallthrough | 	# fallthrough | ||||||
| 	# | 	# | ||||||
| 	# If a query for a record in the cluster zone results in NXDOMAIN, | 	# If a query for a record in the cluster zone results in NXDOMAIN, | ||||||
|   | |||||||
| @@ -2,9 +2,11 @@ package kubernetes | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/coredns/coredns/middleware" | 	"github.com/coredns/coredns/middleware" | ||||||
| 	"github.com/coredns/coredns/middleware/pkg/dnsutil" | 	"github.com/coredns/coredns/middleware/pkg/dnsutil" | ||||||
|  | 	"github.com/coredns/coredns/middleware/rewrite" | ||||||
| 	"github.com/coredns/coredns/request" | 	"github.com/coredns/coredns/request" | ||||||
|  |  | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/dns" | ||||||
| @@ -39,37 +41,55 @@ func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.M | |||||||
| 		zone = state.Name() | 		zone = state.Name() | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var ( | 	records, extra, _, err := k.routeRequest(zone, state) | ||||||
| 		records, extra []dns.RR |  | ||||||
| 		err            error | 	if k.AutoPath.Enabled && k.IsNameError(err) { | ||||||
| 	) | 		p := k.findPodWithIP(state.IP()) | ||||||
| 	switch state.Type() { | 		for p != nil { | ||||||
| 	case "A": | 			name, path, ok := splitSearch(zone, state.QName(), p.Namespace) | ||||||
| 		records, _, err = middleware.A(&k, zone, state, nil, middleware.Options{}) | 			if !ok { | ||||||
| 	case "AAAA": |  | ||||||
| 		records, _, err = middleware.AAAA(&k, zone, state, nil, middleware.Options{}) |  | ||||||
| 	case "TXT": |  | ||||||
| 		records, _, err = middleware.TXT(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "CNAME": |  | ||||||
| 		records, _, err = middleware.CNAME(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "PTR": |  | ||||||
| 		records, _, err = middleware.PTR(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "MX": |  | ||||||
| 		records, extra, _, err = middleware.MX(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "SRV": |  | ||||||
| 		records, extra, _, err = middleware.SRV(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "SOA": |  | ||||||
| 		records, _, err = middleware.SOA(&k, zone, state, middleware.Options{}) |  | ||||||
| 	case "NS": |  | ||||||
| 		if state.Name() == zone { |  | ||||||
| 			records, extra, _, err = middleware.NS(&k, zone, state, middleware.Options{}) |  | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		fallthrough | 			if (dns.CountLabel(name) - 1) < k.AutoPath.NDots { | ||||||
| 	default: | 				break | ||||||
| 		// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN |  | ||||||
| 		_, _, err = middleware.A(&k, zone, state, nil, middleware.Options{}) |  | ||||||
| 			} | 			} | ||||||
|  | 			// Search "svc.cluster.local" and "cluster.local" | ||||||
|  | 			for i := 0; i < 2; i++ { | ||||||
|  | 				path = strings.Join(dns.SplitDomainName(path)[1:], ".") | ||||||
|  | 				state = state.NewWithQuestion(strings.Join([]string{name, path}, "."), state.QType()) | ||||||
|  | 				records, extra, _, err = k.routeRequest(zone, state) | ||||||
|  | 				if !k.IsNameError(err) { | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if !k.IsNameError(err) { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			// Fallthrough with the host search path (if set) | ||||||
|  | 			wr := rewrite.NewResponseReverter(w, r) | ||||||
|  | 			for _, hostsearch := range k.AutoPath.HostSearchPath { | ||||||
|  | 				r = state.NewWithQuestion(strings.Join([]string{name, hostsearch}, "."), state.QType()).Req | ||||||
|  | 				rcode, nextErr := middleware.NextOrFailure(k.Name(), k.Next, ctx, wr, r) | ||||||
|  | 				if rcode == dns.RcodeSuccess { | ||||||
|  | 					return rcode, nextErr | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			// Search . in this middleware | ||||||
|  | 			state = state.NewWithQuestion(strings.Join([]string{name, "."}, ""), state.QType()) | ||||||
|  | 			records, extra, _, err = k.routeRequest(zone, state) | ||||||
|  | 			if !k.IsNameError(err) { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			// Search . in the next middleware | ||||||
|  | 			r = state.Req | ||||||
|  | 			rcode, nextErr := middleware.NextOrFailure(k.Name(), k.Next, ctx, wr, r) | ||||||
|  | 			if rcode == dns.RcodeNameError { | ||||||
|  | 				rcode = k.AutoPath.OnNXDOMAIN | ||||||
|  | 			} | ||||||
|  | 			return rcode, nextErr | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if k.IsNameError(err) { | 	if k.IsNameError(err) { | ||||||
| 		if k.Fallthrough { | 		if k.Fallthrough { | ||||||
| 			return middleware.NextOrFailure(k.Name(), k.Next, ctx, w, r) | 			return middleware.NextOrFailure(k.Name(), k.Next, ctx, w, r) | ||||||
| @@ -95,5 +115,36 @@ func (k Kubernetes) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.M | |||||||
| 	return dns.RcodeSuccess, nil | 	return dns.RcodeSuccess, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (k *Kubernetes) routeRequest(zone string, state request.Request) (records []dns.RR, extra []dns.RR, debug []dns.RR, err error) { | ||||||
|  | 	switch state.Type() { | ||||||
|  | 	case "A": | ||||||
|  | 		records, _, err = middleware.A(k, zone, state, nil, middleware.Options{}) | ||||||
|  | 	case "AAAA": | ||||||
|  | 		records, _, err = middleware.AAAA(k, zone, state, nil, middleware.Options{}) | ||||||
|  | 	case "TXT": | ||||||
|  | 		records, _, err = middleware.TXT(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "CNAME": | ||||||
|  | 		records, _, err = middleware.CNAME(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "PTR": | ||||||
|  | 		records, _, err = middleware.PTR(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "MX": | ||||||
|  | 		records, extra, _, err = middleware.MX(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "SRV": | ||||||
|  | 		records, extra, _, err = middleware.SRV(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "SOA": | ||||||
|  | 		records, _, err = middleware.SOA(k, zone, state, middleware.Options{}) | ||||||
|  | 	case "NS": | ||||||
|  | 		if state.Name() == zone { | ||||||
|  | 			records, extra, _, err = middleware.NS(k, zone, state, middleware.Options{}) | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		fallthrough | ||||||
|  | 	default: | ||||||
|  | 		// Do a fake A lookup, so we can distinguish between NODATA and NXDOMAIN | ||||||
|  | 		_, _, err = middleware.A(k, zone, state, nil, middleware.Options{}) | ||||||
|  | 	} | ||||||
|  | 	return records, extra, nil, err | ||||||
|  | } | ||||||
|  |  | ||||||
| // Name implements the Handler interface. | // Name implements the Handler interface. | ||||||
| func (k Kubernetes) Name() string { return "kubernetes" } | func (k Kubernetes) Name() string { return "kubernetes" } | ||||||
|   | |||||||
| @@ -45,9 +45,18 @@ type Kubernetes struct { | |||||||
| 	PodMode       string | 	PodMode       string | ||||||
| 	ReverseCidrs  []net.IPNet | 	ReverseCidrs  []net.IPNet | ||||||
| 	Fallthrough   bool | 	Fallthrough   bool | ||||||
|  | 	AutoPath | ||||||
| 	interfaceAddrs interfaceAddrser | 	interfaceAddrs interfaceAddrser | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type AutoPath struct { | ||||||
|  | 	Enabled        bool | ||||||
|  | 	NDots          int | ||||||
|  | 	ResolvConfFile string | ||||||
|  | 	HostSearchPath []string | ||||||
|  | 	OnNXDOMAIN     int | ||||||
|  | } | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| 	// PodModeDisabled is the default value where pod requests are ignored | 	// PodModeDisabled is the default value where pod requests are ignored | ||||||
| 	PodModeDisabled = "disabled" | 	PodModeDisabled = "disabled" | ||||||
| @@ -97,6 +106,7 @@ var errInvalidRequest = errors.New("invalid query name") | |||||||
| var errZoneNotFound = errors.New("zone not found") | var errZoneNotFound = errors.New("zone not found") | ||||||
| var errAPIBadPodType = errors.New("expected type *api.Pod") | var errAPIBadPodType = errors.New("expected type *api.Pod") | ||||||
| var errPodsDisabled = errors.New("pod records disabled") | var errPodsDisabled = errors.New("pod records disabled") | ||||||
|  | var errResolvConfReadErr = errors.New("resolv.conf read error") | ||||||
|  |  | ||||||
| // Services implements the ServiceBackend interface. | // 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) { | func (k *Kubernetes) Services(state request.Request, exact bool, opt middleware.Options) (svcs []msg.Service, debug []msg.Service, err error) { | ||||||
| @@ -183,7 +193,7 @@ func (k *Kubernetes) Lookup(state request.Request, name string, typ uint16) (*dn | |||||||
|  |  | ||||||
| // IsNameError implements the ServiceBackend interface. | // IsNameError implements the ServiceBackend interface. | ||||||
| func (k *Kubernetes) IsNameError(err error) bool { | func (k *Kubernetes) IsNameError(err error) bool { | ||||||
| 	return err == errNoItems || err == errNsNotExposed || err == errInvalidRequest | 	return err == errNoItems || err == errNsNotExposed || err == errInvalidRequest || err == errZoneNotFound | ||||||
| } | } | ||||||
|  |  | ||||||
| // Debug implements the ServiceBackend interface. | // Debug implements the ServiceBackend interface. | ||||||
| @@ -245,7 +255,7 @@ func (k *Kubernetes) InitKubeCache() (err error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts := dnsControlOpts{ | 	opts := dnsControlOpts{ | ||||||
| 		initPodCache: k.PodMode == PodModeVerified, | 		initPodCache: (k.PodMode == PodModeVerified || k.AutoPath.Enabled), | ||||||
| 	} | 	} | ||||||
| 	k.APIConn = newdnsController(kubeClient, k.ResyncPeriod, k.Selector, opts) | 	k.APIConn = newdnsController(kubeClient, k.ResyncPeriod, k.Selector, opts) | ||||||
|  |  | ||||||
| @@ -448,6 +458,21 @@ func ipFromPodName(podname string) string { | |||||||
| 	return strings.Replace(podname, "-", ":", -1) | 	return strings.Replace(podname, "-", ":", -1) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (k *Kubernetes) findPodWithIP(ip string) (p *api.Pod) { | ||||||
|  | 	if k.PodMode != PodModeVerified { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	objList := k.APIConn.PodIndex(ip) | ||||||
|  | 	for _, o := range objList { | ||||||
|  | 		p, ok := o.(*api.Pod) | ||||||
|  | 		if !ok { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 		return p | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) { | func (k *Kubernetes) findPods(namespace, podname string) (pods []pod, err error) { | ||||||
| 	if k.PodMode == PodModeDisabled { | 	if k.PodMode == PodModeDisabled { | ||||||
| 		return pods, errPodsDisabled | 		return pods, errPodsDisabled | ||||||
| @@ -634,3 +659,11 @@ func (k *Kubernetes) localPodIP() net.IP { | |||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func splitSearch(zone, question, namespace string) (name, search string, ok bool) { | ||||||
|  | 	search = strings.Join([]string{namespace, "svc", zone}, ".") | ||||||
|  | 	if dns.IsSubDomain(search, question) { | ||||||
|  | 		return question[:len(question)-len(search)-1], search, true | ||||||
|  | 	} | ||||||
|  | 	return "", "", false | ||||||
|  | } | ||||||
|   | |||||||
| @@ -480,3 +480,27 @@ func TestServices(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestSplitSearchPath(t *testing.T) { | ||||||
|  | 	type testCase struct { | ||||||
|  | 		question       string | ||||||
|  | 		namespace      string | ||||||
|  | 		expectedName   string | ||||||
|  | 		expectedSearch string | ||||||
|  | 		expectedOk     bool | ||||||
|  | 	} | ||||||
|  | 	tests := []testCase{ | ||||||
|  | 		{question: "test.blah.com", namespace: "ns1", expectedName: "", expectedSearch: "", expectedOk: false}, | ||||||
|  | 		{question: "foo.com.ns2.svc.interwebs.nets", namespace: "ns1", expectedName: "", expectedSearch: "", expectedOk: false}, | ||||||
|  | 		{question: "foo.com.svc.interwebs.nets", namespace: "ns1", expectedName: "", expectedSearch: "", expectedOk: false}, | ||||||
|  | 		{question: "foo.com.ns1.svc.interwebs.nets", namespace: "ns1", expectedName: "foo.com", expectedSearch: "ns1.svc.interwebs.nets", expectedOk: true}, | ||||||
|  | 	} | ||||||
|  | 	zone := "interwebs.nets" | ||||||
|  | 	for _, c := range tests { | ||||||
|  | 		name, search, ok := splitSearch(zone, c.question, c.namespace) | ||||||
|  | 		if c.expectedName != name || c.expectedSearch != search || c.expectedOk != ok { | ||||||
|  | 			t.Errorf("Case %v: Expected name'%v', search:'%v', ok:'%v'. Got name:'%v', search:'%v', ok:'%v'.", c.question, c.expectedName, c.expectedSearch, c.expectedOk, name, search, ok) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -11,6 +12,7 @@ import ( | |||||||
| 	"github.com/coredns/coredns/middleware" | 	"github.com/coredns/coredns/middleware" | ||||||
| 	"github.com/coredns/coredns/middleware/pkg/dnsutil" | 	"github.com/coredns/coredns/middleware/pkg/dnsutil" | ||||||
| 	"github.com/coredns/coredns/middleware/proxy" | 	"github.com/coredns/coredns/middleware/proxy" | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  |  | ||||||
| 	"github.com/mholt/caddy" | 	"github.com/mholt/caddy" | ||||||
| 	unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned" | 	unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned" | ||||||
| @@ -187,8 +189,49 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { | |||||||
| 						continue | 						continue | ||||||
| 					} | 					} | ||||||
| 					return nil, fmt.Errorf("incorrect number of arguments for federation, got %v, expected 2", len(args)) | 					return nil, fmt.Errorf("incorrect number of arguments for federation, got %v, expected 2", len(args)) | ||||||
|  | 				case "autopath": // name zone | ||||||
|  | 					args := c.RemainingArgs() | ||||||
|  | 					k8s.AutoPath = AutoPath{ | ||||||
|  | 						NDots:          defautNdots, | ||||||
|  | 						HostSearchPath: []string{}, | ||||||
|  | 						ResolvConfFile: defaultResolvConfFile, | ||||||
|  | 						OnNXDOMAIN:     defaultOnNXDOMAIN, | ||||||
|  | 					} | ||||||
|  | 					if len(args) > 3 { | ||||||
|  | 						return nil, fmt.Errorf("incorrect number of arguments for autopath, got %v, expected at most 3", len(args)) | ||||||
|  |  | ||||||
| 					} | 					} | ||||||
|  | 					if len(args) > 0 { | ||||||
|  | 						ndots, err := strconv.Atoi(args[0]) | ||||||
|  | 						if err != nil { | ||||||
|  | 							return nil, fmt.Errorf("invalid NDOTS argument for autopath, got '%v', expected an integer", ndots) | ||||||
|  | 						} | ||||||
|  | 						k8s.AutoPath.NDots = ndots | ||||||
|  | 					} | ||||||
|  | 					if len(args) > 1 { | ||||||
|  | 						switch args[1] { | ||||||
|  | 						case dns.RcodeToString[dns.RcodeNameError]: | ||||||
|  | 							k8s.AutoPath.OnNXDOMAIN = dns.RcodeNameError | ||||||
|  | 						case dns.RcodeToString[dns.RcodeSuccess]: | ||||||
|  | 							k8s.AutoPath.OnNXDOMAIN = dns.RcodeSuccess | ||||||
|  | 						case dns.RcodeToString[dns.RcodeServerFailure]: | ||||||
|  | 							k8s.AutoPath.OnNXDOMAIN = dns.RcodeServerFailure | ||||||
|  | 						default: | ||||||
|  | 							return nil, fmt.Errorf("invalid RESPONSE argument for autopath, got '%v', expected SERVFAIL, NOERROR, or NXDOMAIN", args[1]) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					if len(args) > 2 { | ||||||
|  | 						k8s.AutoPath.ResolvConfFile = args[2] | ||||||
|  | 					} | ||||||
|  | 					rc, err := dns.ClientConfigFromFile(k8s.AutoPath.ResolvConfFile) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return nil, fmt.Errorf("error when parsing %v: %v", k8s.AutoPath.ResolvConfFile, err) | ||||||
|  | 					} | ||||||
|  | 					k8s.AutoPath.HostSearchPath = rc.Search | ||||||
|  | 					middleware.Zones(k8s.AutoPath.HostSearchPath).Normalize() | ||||||
|  | 					k8s.AutoPath.Enabled = true | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 			return k8s, nil | 			return k8s, nil | ||||||
| 		} | 		} | ||||||
| @@ -199,4 +242,7 @@ func kubernetesParse(c *caddy.Controller) (*Kubernetes, error) { | |||||||
| const ( | const ( | ||||||
| 	defaultResyncPeriod   = 5 * time.Minute | 	defaultResyncPeriod   = 5 * time.Minute | ||||||
| 	defaultPodMode        = PodModeDisabled | 	defaultPodMode        = PodModeDisabled | ||||||
|  | 	defautNdots           = 0 | ||||||
|  | 	defaultResolvConfFile = "/etc/resolv.conf" | ||||||
|  | 	defaultOnNXDOMAIN     = dns.RcodeServerFailure | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -2,11 +2,16 @@ package kubernetes | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"net" | 	"net" | ||||||
|  | 	"os" | ||||||
|  | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/coredns/coredns/middleware/test" | ||||||
|  |  | ||||||
| 	"github.com/mholt/caddy" | 	"github.com/mholt/caddy" | ||||||
|  | 	"github.com/miekg/dns" | ||||||
| 	unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned" | 	unversionedapi "k8s.io/client-go/1.5/pkg/api/unversioned" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -16,6 +21,13 @@ func parseCidr(cidr string) net.IPNet { | |||||||
| } | } | ||||||
|  |  | ||||||
| func TestKubernetesParse(t *testing.T) { | func TestKubernetesParse(t *testing.T) { | ||||||
|  | 	f, rm, err := test.TempFile(os.TempDir(), testResolveConf) | ||||||
|  | 	autoPathResolvConfFile := f | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Could not create resolv.conf TempFile: %s", err) | ||||||
|  | 	} | ||||||
|  | 	defer rm() | ||||||
|  |  | ||||||
| 	tests := []struct { | 	tests := []struct { | ||||||
| 		description           string        // Human-facing description of test case | 		description           string        // Human-facing description of test case | ||||||
| 		input                 string        // Corefile data as string | 		input                 string        // Corefile data as string | ||||||
| @@ -30,6 +42,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 		expectedFallthrough   bool | 		expectedFallthrough   bool | ||||||
| 		expectedUpstreams     []string | 		expectedUpstreams     []string | ||||||
| 		expectedFederations   []Federation | 		expectedFederations   []Federation | ||||||
|  | 		expectedAutoPath      AutoPath | ||||||
| 	}{ | 	}{ | ||||||
| 		// positive | 		// positive | ||||||
| 		{ | 		{ | ||||||
| @@ -46,6 +59,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"kubernetes keyword with multiple zones", | 			"kubernetes keyword with multiple zones", | ||||||
| @@ -61,6 +75,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"kubernetes keyword with zone and empty braces", | 			"kubernetes keyword with zone and empty braces", | ||||||
| @@ -77,6 +92,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"endpoint keyword with url", | 			"endpoint keyword with url", | ||||||
| @@ -94,6 +110,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"namespaces keyword with one namespace", | 			"namespaces keyword with one namespace", | ||||||
| @@ -111,6 +128,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			nil, | 			nil, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"namespaces keyword with multiple namespaces", | 			"namespaces keyword with multiple namespaces", | ||||||
| @@ -128,6 +146,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"resync period in seconds", | 			"resync period in seconds", | ||||||
| @@ -145,6 +164,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"resync period in minutes", | 			"resync period in minutes", | ||||||
| @@ -162,6 +182,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"basic label selector", | 			"basic label selector", | ||||||
| @@ -179,6 +200,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"multi-label selector", | 			"multi-label selector", | ||||||
| @@ -196,6 +218,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"fully specified valid config", | 			"fully specified valid config", | ||||||
| @@ -217,6 +240,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			true, | 			true, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// negative | 		// negative | ||||||
| 		{ | 		{ | ||||||
| @@ -233,6 +257,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"kubernetes keyword without a zone", | 			"kubernetes keyword without a zone", | ||||||
| @@ -248,6 +273,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"endpoint keyword without an endpoint value", | 			"endpoint keyword without an endpoint value", | ||||||
| @@ -265,6 +291,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"namespace keyword without a namespace value", | 			"namespace keyword without a namespace value", | ||||||
| @@ -282,6 +309,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"resyncperiod keyword without a duration value", | 			"resyncperiod keyword without a duration value", | ||||||
| @@ -299,6 +327,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"resync period no units", | 			"resync period no units", | ||||||
| @@ -316,6 +345,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"resync period invalid", | 			"resync period invalid", | ||||||
| @@ -333,6 +363,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"labels with no selector value", | 			"labels with no selector value", | ||||||
| @@ -350,6 +381,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"labels with invalid selector value", | 			"labels with invalid selector value", | ||||||
| @@ -367,6 +399,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// pods disabled | 		// pods disabled | ||||||
| 		{ | 		{ | ||||||
| @@ -385,6 +418,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// pods insecure | 		// pods insecure | ||||||
| 		{ | 		{ | ||||||
| @@ -403,6 +437,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// pods verified | 		// pods verified | ||||||
| 		{ | 		{ | ||||||
| @@ -421,6 +456,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// pods invalid | 		// pods invalid | ||||||
| 		{ | 		{ | ||||||
| @@ -439,6 +475,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// cidrs ok | 		// cidrs ok | ||||||
| 		{ | 		{ | ||||||
| @@ -457,6 +494,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// cidrs ok | 		// cidrs ok | ||||||
| 		{ | 		{ | ||||||
| @@ -475,6 +513,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// fallthrough invalid | 		// fallthrough invalid | ||||||
| 		{ | 		{ | ||||||
| @@ -493,6 +532,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// Valid upstream | 		// Valid upstream | ||||||
| 		{ | 		{ | ||||||
| @@ -511,6 +551,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			[]string{"13.14.15.16:53"}, | 			[]string{"13.14.15.16:53"}, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// Invalid upstream | 		// Invalid upstream | ||||||
| 		{ | 		{ | ||||||
| @@ -529,6 +570,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// Valid federations | 		// Valid federations | ||||||
| 		{ | 		{ | ||||||
| @@ -551,6 +593,7 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 				{name: "foo", zone: "bar.crawl.com"}, | 				{name: "foo", zone: "bar.crawl.com"}, | ||||||
| 				{name: "fed", zone: "era.tion.com"}, | 				{name: "fed", zone: "era.tion.com"}, | ||||||
| 			}, | 			}, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 		// Invalid federations | 		// Invalid federations | ||||||
| 		{ | 		{ | ||||||
| @@ -569,6 +612,104 @@ func TestKubernetesParse(t *testing.T) { | |||||||
| 			false, | 			false, | ||||||
| 			nil, | 			nil, | ||||||
| 			[]Federation{}, | 			[]Federation{}, | ||||||
|  | 			AutoPath{}, | ||||||
|  | 		}, | ||||||
|  | 		// autopath | ||||||
|  | 		{ | ||||||
|  | 			"valid autopath", | ||||||
|  | 			`kubernetes coredns.local { | ||||||
|  | 	autopath 1 NXDOMAIN ` + autoPathResolvConfFile + ` | ||||||
|  | }`, | ||||||
|  | 			false, | ||||||
|  | 			"", | ||||||
|  | 			1, | ||||||
|  | 			0, | ||||||
|  | 			defaultResyncPeriod, | ||||||
|  | 			"", | ||||||
|  | 			defaultPodMode, | ||||||
|  | 			nil, | ||||||
|  | 			false, | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
|  | 			AutoPath{ | ||||||
|  | 				Enabled:        true, | ||||||
|  | 				NDots:          1, | ||||||
|  | 				HostSearchPath: []string{"bar.com.", "baz.com."}, | ||||||
|  | 				ResolvConfFile: autoPathResolvConfFile, | ||||||
|  | 				OnNXDOMAIN:     dns.RcodeNameError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"invalid autopath RESPONSE", | ||||||
|  | 			`kubernetes coredns.local { | ||||||
|  | 	autopath 0 CRY | ||||||
|  | }`, | ||||||
|  | 			true, | ||||||
|  | 			"invalid RESPONSE argument for autopath", | ||||||
|  | 			-1, | ||||||
|  | 			0, | ||||||
|  | 			defaultResyncPeriod, | ||||||
|  | 			"", | ||||||
|  | 			defaultPodMode, | ||||||
|  | 			nil, | ||||||
|  | 			false, | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
|  | 			AutoPath{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"invalid autopath NDOTS", | ||||||
|  | 			`kubernetes coredns.local { | ||||||
|  | 	autopath polka | ||||||
|  | }`, | ||||||
|  | 			true, | ||||||
|  | 			"invalid NDOTS argument for autopath", | ||||||
|  | 			-1, | ||||||
|  | 			0, | ||||||
|  | 			defaultResyncPeriod, | ||||||
|  | 			"", | ||||||
|  | 			defaultPodMode, | ||||||
|  | 			nil, | ||||||
|  | 			false, | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
|  | 			AutoPath{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"invalid autopath RESOLV-CONF", | ||||||
|  | 			`kubernetes coredns.local { | ||||||
|  | 	autopath 1 NOERROR /wrong/path/to/resolv.conf | ||||||
|  | }`, | ||||||
|  | 			true, | ||||||
|  | 			"error when parsing", | ||||||
|  | 			-1, | ||||||
|  | 			0, | ||||||
|  | 			defaultResyncPeriod, | ||||||
|  | 			"", | ||||||
|  | 			defaultPodMode, | ||||||
|  | 			nil, | ||||||
|  | 			false, | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
|  | 			AutoPath{}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"invalid autopath invalid option", | ||||||
|  | 			`kubernetes coredns.local { | ||||||
|  | 	autopath 1 SERVFAIL ` + autoPathResolvConfFile + ` foo | ||||||
|  | }`, | ||||||
|  | 			true, | ||||||
|  | 			"incorrect number of arguments", | ||||||
|  | 			-1, | ||||||
|  | 			0, | ||||||
|  | 			defaultResyncPeriod, | ||||||
|  | 			"", | ||||||
|  | 			defaultPodMode, | ||||||
|  | 			nil, | ||||||
|  | 			false, | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
|  | 			AutoPath{}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -669,6 +810,15 @@ func TestKubernetesParse(t *testing.T) { | |||||||
|  |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		// autopath | ||||||
|  | 		if !reflect.DeepEqual(test.expectedAutoPath, k8sController.AutoPath) { | ||||||
|  | 			t.Errorf("Test %d: Expected kubernetes controller to be initialized with autopath '%v'. Instead found autopath '%v' for input '%s'", i, test.expectedAutoPath, k8sController.AutoPath, test.input) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	} | const testResolveConf = `nameserver 1.2.3.4 | ||||||
| } | domain foo.com | ||||||
|  | search bar.com baz.com | ||||||
|  | options ndots:5 | ||||||
|  | ` | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user