mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	Adding resyncperiod to Corefile (#205)
				
					
				
			* Removing old unused inline k8s API code and tests. * Adding parsing implementation for `resyncperiod` keyword from Corefile. * Adding tests for parsing `resyncperiod` keyword from Corefile. 8 Updating README.md and conf/k8sCorefile.
This commit is contained in:
		| @@ -35,6 +35,9 @@ This is the default kubernetes setup, with everything specified in full: | ||||
| .:53 { | ||||
|     # use kubernetes middleware for domain "coredns.local" | ||||
|     kubernetes coredns.local { | ||||
|         # Kubernetes data API resync period | ||||
|         # Example values: 60s, 5m, 1h | ||||
|         resyncperiod 5m | ||||
|         # Use url for k8s API endpoint | ||||
|         endpoint http://localhost:8080 | ||||
|         # Assemble k8s record names with the template | ||||
| @@ -42,10 +45,17 @@ This is the default kubernetes setup, with everything specified in full: | ||||
|         # Only expose the k8s namespace "demo" | ||||
|         namespaces demo | ||||
|     } | ||||
| #    cache 160 coredns.local | ||||
|     # Perform DNS response caching for the coredns.local zone | ||||
|     # Cache timeout is provided by the integer in seconds | ||||
|     #cache 180 coredns.local | ||||
| } | ||||
| ~~~ | ||||
|  | ||||
| Notes: | ||||
| * If the `namespaces` keyword is omitted, all kubernetes namespaces are exposed. | ||||
| * If the `template` keyword is omitted, the default template of "{service}.{namespace}.{zone}" is used. | ||||
| * If the `resyncperiod` keyword is omitted, the default resync period is 5 minutes. | ||||
|  | ||||
| ### Basic Setup | ||||
|  | ||||
| #### Launch Kubernetes | ||||
| @@ -305,14 +315,9 @@ TBD: | ||||
| 	* Performance | ||||
| 		* Improve lookup to reduce size of query result obtained from k8s API. | ||||
| 		  (namespace-based?, other ideas?) | ||||
| 		* Caching/notification of k8s API dataset. (See aledbf fork for | ||||
| 		  implementation ideas.) | ||||
| 		* DNS response caching is good, but we should also cache at the http query  | ||||
| 		  level as well. (Take a look at https://github.com/patrickmn/go-cache as  | ||||
| 		  a potential expiring cache implementation for the http API queries.) | ||||
| * Additional features: | ||||
| 	* Reverse IN-ADDR entries for services. (Is there any value in supporting  | ||||
| 	  reverse lookup records?) | ||||
| 	  reverse lookup records?) (need tests, functionality should work based on @aledbf's code.) | ||||
| 	* How to support label specification in Corefile to allow use of labels to  | ||||
| 	  indicate zone? (Is this even useful?) For example, the following | ||||
| 	  configuration exposes all services labeled for the "staging" environment | ||||
| @@ -333,14 +338,6 @@ TBD: | ||||
| 	  flattening to lower case and mapping of non-DNS characters to DNS characters | ||||
| 	  in a standard way.) | ||||
| 	* Expose arbitrary kubernetes repository data as TXT records? | ||||
| 	* (done) ~~Support custom user-provided templates for k8s names. A string provided | ||||
| 	  in the middleware configuration like `{service}.{namespace}.{type}` defines | ||||
| 	  the template of how to construct record names for the zone. This example | ||||
| 	  would produce `myservice.mynamespace.svc.cluster.local`. (Basic template | ||||
| 	  implemented. Need to slice zone out of current template implementation.)~~ | ||||
| 	* (done) ~~Implement namespace filtering to different zones. That is, zone "a.b" | ||||
| 	  publishes services from namespace "foo", and zone "x.y" publishes services | ||||
| 	  from namespaces "bar" and "baz". (Basic version implemented -- need test cases.)~~ | ||||
| * DNS Correctness | ||||
| 	* Do we need to generate synthetic zone records for namespaces? | ||||
| 	* Do we need to generate synthetic zone records for the skydns synthetic zones? | ||||
| @@ -352,7 +349,3 @@ TBD: | ||||
| 	  pre-loaded k8s API cache. With and without CoreDNS response caching. | ||||
|     * Try to get rid of kubernetes launch scripts by moving operations into | ||||
|       .travis.yml file. | ||||
| 	* ~~Implement test cases for http data parsing using dependency injection | ||||
| 	  for http get operations.~~ | ||||
|     * ~~Automate integration testing with kubernetes. (k8s launch and service | ||||
|       start-up automation is in middleware/kubernetes/tests)~~ | ||||
|   | ||||
| @@ -129,7 +129,7 @@ func (dns *dnsController) Stop() error { | ||||
|  | ||||
| // Run starts the controller. | ||||
| func (dns *dnsController) Run() { | ||||
| 	log.Println("[debug] starting coredns controller") | ||||
| 	log.Println("[debug] Starting k8s notification controllers") | ||||
|  | ||||
| 	go dns.endpController.Run(dns.stopCh) | ||||
| 	go dns.svcController.Run(dns.stopCh) | ||||
|   | ||||
| @@ -1,113 +0,0 @@ | ||||
| package k8sclient | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| // getK8sAPIResponse wraps the http.Get(url) function to provide dependency | ||||
| // injection for unit testing. | ||||
| var getK8sAPIResponse = func(url string) (resp *http.Response, err error) { | ||||
| 	resp, err = http.Get(url) | ||||
| 	return resp, err | ||||
| } | ||||
|  | ||||
| func parseJson(url string, target interface{}) error { | ||||
| 	r, err := getK8sAPIResponse(url) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer r.Body.Close() | ||||
|  | ||||
| 	return json.NewDecoder(r.Body).Decode(target) | ||||
| } | ||||
|  | ||||
| // Kubernetes Resource List | ||||
| type ResourceList struct { | ||||
| 	Kind         string     `json:"kind"` | ||||
| 	GroupVersion string     `json:"groupVersion"` | ||||
| 	Resources    []resource `json:"resources"` | ||||
| } | ||||
|  | ||||
| type resource struct { | ||||
| 	Name       string `json:"name"` | ||||
| 	Namespaced bool   `json:"namespaced"` | ||||
| 	Kind       string `json:"kind"` | ||||
| } | ||||
|  | ||||
| // Kubernetes NamespaceList | ||||
| type NamespaceList struct { | ||||
| 	Kind       string          `json:"kind"` | ||||
| 	APIVersion string          `json:"apiVersion"` | ||||
| 	Metadata   apiListMetadata `json:"metadata"` | ||||
| 	Items      []nsItems       `json:"items"` | ||||
| } | ||||
|  | ||||
| type apiListMetadata struct { | ||||
| 	SelfLink        string `json:"selfLink"` | ||||
| 	ResourceVersion string `json:"resourceVersion"` | ||||
| } | ||||
|  | ||||
| type nsItems struct { | ||||
| 	Metadata nsMetadata `json:"metadata"` | ||||
| 	Spec     nsSpec     `json:"spec"` | ||||
| 	Status   nsStatus   `json:"status"` | ||||
| } | ||||
|  | ||||
| type nsMetadata struct { | ||||
| 	Name              string `json:"name"` | ||||
| 	SelfLink          string `json:"selfLink"` | ||||
| 	Uid               string `json:"uid"` | ||||
| 	ResourceVersion   string `json:"resourceVersion"` | ||||
| 	CreationTimestamp string `json:"creationTimestamp"` | ||||
| } | ||||
|  | ||||
| type nsSpec struct { | ||||
| 	Finalizers []string `json:"finalizers"` | ||||
| } | ||||
|  | ||||
| type nsStatus struct { | ||||
| 	Phase string `json:"phase"` | ||||
| } | ||||
|  | ||||
| // Kubernetes ServiceList | ||||
| type ServiceList struct { | ||||
| 	Kind       string          `json:"kind"` | ||||
| 	APIVersion string          `json:"apiVersion"` | ||||
| 	Metadata   apiListMetadata `json:"metadata"` | ||||
| 	Items      []ServiceItem   `json:"items"` | ||||
| } | ||||
|  | ||||
| type ServiceItem struct { | ||||
| 	Metadata serviceMetadata `json:"metadata"` | ||||
| 	Spec     serviceSpec     `json:"spec"` | ||||
| 	//    Status     serviceStatus    `json:"status"` | ||||
| } | ||||
|  | ||||
| type serviceMetadata struct { | ||||
| 	Name              string `json:"name"` | ||||
| 	Namespace         string `json:"namespace"` | ||||
| 	SelfLink          string `json:"selfLink"` | ||||
| 	Uid               string `json:"uid"` | ||||
| 	ResourceVersion   string `json:"resourceVersion"` | ||||
| 	CreationTimestamp string `json:"creationTimestamp"` | ||||
| 	// labels | ||||
| } | ||||
|  | ||||
| type serviceSpec struct { | ||||
| 	Ports           []servicePort `json:"ports"` | ||||
| 	ClusterIP       string        `json:"clusterIP"` | ||||
| 	Type            string        `json:"type"` | ||||
| 	SessionAffinity string        `json:"sessionAffinity"` | ||||
| } | ||||
|  | ||||
| type servicePort struct { | ||||
| 	Name       string `json:"name"` | ||||
| 	Protocol   string `json:"protocol"` | ||||
| 	Port       int    `json:"port"` | ||||
| 	TargetPort int    `json:"targetPort"` | ||||
| } | ||||
|  | ||||
| type serviceStatus struct { | ||||
| 	LoadBalancer string `json:"loadBalancer"` | ||||
| } | ||||
| @@ -1,133 +0,0 @@ | ||||
| package k8sclient | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"log" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // API strings | ||||
| const ( | ||||
| 	apiBase       = "/api/v1" | ||||
| 	apiNamespaces = "/namespaces" | ||||
| 	apiServices   = "/services" | ||||
| ) | ||||
|  | ||||
| // Defaults | ||||
| const ( | ||||
| 	defaultBaseURL = "http://localhost:8080" | ||||
| ) | ||||
|  | ||||
| type K8sConnector struct { | ||||
| 	baseURL string | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) SetBaseURL(u string) error { | ||||
| 	url, error := url.Parse(u) | ||||
|  | ||||
| 	if error != nil { | ||||
| 		return error | ||||
| 	} | ||||
|  | ||||
| 	if !url.IsAbs() { | ||||
| 		return errors.New("k8sclient: Kubernetes endpoint url must be an absolute URL") | ||||
| 	} | ||||
|  | ||||
| 	c.baseURL = url.String() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetBaseURL() string { | ||||
| 	return c.baseURL | ||||
| } | ||||
|  | ||||
| // URL constructor separated from code to support dependency injection | ||||
| // for unit tests. | ||||
| var makeURL = func(parts []string) string { | ||||
| 	return strings.Join(parts, "") | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetResourceList() (*ResourceList, error) { | ||||
| 	resources := new(ResourceList) | ||||
|  | ||||
| 	url := makeURL([]string{c.baseURL, apiBase}) | ||||
| 	err := parseJson(url, resources) | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if err != nil { | ||||
| 		log.Printf("[ERROR] Response from kubernetes API for GetResourceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return resources, nil | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetNamespaceList() (*NamespaceList, error) { | ||||
| 	namespaces := new(NamespaceList) | ||||
|  | ||||
| 	url := makeURL([]string{c.baseURL, apiBase, apiNamespaces}) | ||||
| 	err := parseJson(url, namespaces) | ||||
| 	if err != nil { | ||||
| 		log.Printf("[ERROR] Response from kubernetes API for GetNamespaceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return namespaces, nil | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetServiceList() (*ServiceList, error) { | ||||
| 	services := new(ServiceList) | ||||
|  | ||||
| 	url := makeURL([]string{c.baseURL, apiBase, apiServices}) | ||||
| 	err := parseJson(url, services) | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if err != nil { | ||||
| 		log.Printf("[ERROR] Response from kubernetes API for GetServiceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| // GetServicesByNamespace returns a map of | ||||
| // namespacename :: [ kubernetesServiceItem ] | ||||
| func (c *K8sConnector) GetServicesByNamespace() (map[string][]ServiceItem, error) { | ||||
|  | ||||
| 	items := make(map[string][]ServiceItem) | ||||
|  | ||||
| 	k8sServiceList, err := c.GetServiceList() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Printf("[ERROR] Getting service list produced error: %v", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if k8sServiceList == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	k8sItemList := k8sServiceList.Items | ||||
|  | ||||
| 	for _, i := range k8sItemList { | ||||
| 		namespace := i.Metadata.Namespace | ||||
| 		items[namespace] = append(items[namespace], i) | ||||
| 	} | ||||
|  | ||||
| 	return items, nil | ||||
| } | ||||
|  | ||||
| func NewK8sConnector(baseURL string) *K8sConnector { | ||||
| 	k := new(K8sConnector) | ||||
|  | ||||
| 	if baseURL == "" { | ||||
| 		baseURL = defaultBaseURL | ||||
| 	} | ||||
|  | ||||
| 	err := k.SetBaseURL(baseURL) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return k | ||||
| } | ||||
| @@ -1,680 +0,0 @@ | ||||
| package k8sclient | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| var validURLs = []string{ | ||||
| 	"http://www.github.com", | ||||
| 	"http://www.github.com:8080", | ||||
| 	"http://8.8.8.8", | ||||
| 	"http://8.8.8.8:9090", | ||||
| 	"www.github.com:8080", | ||||
| } | ||||
|  | ||||
| var invalidURLs = []string{ | ||||
| 	"www.github.com", | ||||
| 	"8.8.8.8", | ||||
| 	"8.8.8.8:1010", | ||||
| 	"8.8`8.8", | ||||
| } | ||||
|  | ||||
| func TestNewK8sConnector(t *testing.T) { | ||||
| 	var conn *K8sConnector | ||||
| 	var url string | ||||
|  | ||||
| 	// Create with empty URL | ||||
| 	conn = nil | ||||
| 	url = "" | ||||
|  | ||||
| 	conn = NewK8sConnector("") | ||||
| 	if conn == nil { | ||||
| 		t.Errorf("Expected K8sConnector instance. Instead got '%v'", conn) | ||||
| 	} | ||||
| 	url = conn.GetBaseURL() | ||||
| 	if url != defaultBaseURL { | ||||
| 		t.Errorf("Expected K8sConnector instance to be initialized with defaultBaseURL. Instead got '%v'", url) | ||||
| 	} | ||||
|  | ||||
| 	// Create with valid URL | ||||
| 	for _, validURL := range validURLs { | ||||
| 		conn = nil | ||||
| 		url = "" | ||||
|  | ||||
| 		conn = NewK8sConnector(validURL) | ||||
| 		if conn == nil { | ||||
| 			t.Errorf("Expected K8sConnector instance. Instead got '%v'", conn) | ||||
| 		} | ||||
| 		url = conn.GetBaseURL() | ||||
| 		if url != validURL { | ||||
| 			t.Errorf("Expected K8sConnector instance to be initialized with supplied url '%v'. Instead got '%v'", validURL, url) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Create with invalid URL | ||||
| 	for _, invalidURL := range invalidURLs { | ||||
| 		conn = nil | ||||
| 		url = "" | ||||
|  | ||||
| 		conn = NewK8sConnector(invalidURL) | ||||
| 		if conn != nil { | ||||
| 			t.Errorf("Expected to not get K8sConnector instance. Instead got '%v'", conn) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestSetBaseURL(t *testing.T) { | ||||
| 	// SetBaseURL with valid URLs should work... | ||||
| 	for _, validURL := range validURLs { | ||||
| 		conn := NewK8sConnector(defaultBaseURL) | ||||
| 		err := conn.SetBaseURL(validURL) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("Expected to receive nil, instead got error '%v'", err) | ||||
| 			continue | ||||
| 		} | ||||
| 		url := conn.GetBaseURL() | ||||
| 		if url != validURL { | ||||
| 			t.Errorf("Expected to connector url to be set to value '%v', instead set to '%v'", validURL, url) | ||||
| 			continue | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// SetBaseURL with invalid or non absolute URLs should not change state... | ||||
| 	for _, invalidURL := range invalidURLs { | ||||
| 		conn := NewK8sConnector(defaultBaseURL) | ||||
| 		originalURL := conn.GetBaseURL() | ||||
|  | ||||
| 		err := conn.SetBaseURL(invalidURL) | ||||
| 		if err == nil { | ||||
| 			t.Errorf("Expected to receive an error value, instead got nil") | ||||
| 		} | ||||
| 		url := conn.GetBaseURL() | ||||
| 		if url != originalURL { | ||||
| 			t.Errorf("Expected base url to not change, instead it changed to '%v'", url) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetNamespaceList(t *testing.T) { | ||||
| 	// Set up a test http server | ||||
| 	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		fmt.Fprintln(w, namespaceListJsonData) | ||||
| 	})) | ||||
| 	defer testServer.Close() | ||||
|  | ||||
| 	// Overwrite URL constructor to access testServer | ||||
| 	makeURL = func(parts []string) string { | ||||
| 		return testServer.URL | ||||
| 	} | ||||
|  | ||||
| 	expectedNamespaces := []string{"default", "demo", "test"} | ||||
| 	apiConn := NewK8sConnector("") | ||||
| 	namespaceList, err := apiConn.GetNamespaceList() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error from from GetNamespaceList(), instead got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if namespaceList == nil { | ||||
| 		t.Errorf("Expected data from GetNamespaceList(), instead got nil") | ||||
| 	} | ||||
|  | ||||
| 	kind := namespaceList.Kind | ||||
| 	if kind != "NamespaceList" { | ||||
| 		t.Errorf("Expected data from GetNamespaceList() to have Kind='NamespaceList', instead got Kind='%v'", kind) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure correct number of namespaces found | ||||
| 	expectedCount := len(expectedNamespaces) | ||||
| 	namespaceCount := len(namespaceList.Items) | ||||
| 	if namespaceCount != expectedCount { | ||||
| 		t.Errorf("Expected '%v' namespaces from GetNamespaceList(), instead found '%v' namespaces", expectedCount, namespaceCount) | ||||
| 	} | ||||
|  | ||||
| 	// Check that all expectedNamespaces are found in the parsed data | ||||
| 	for _, ns := range expectedNamespaces { | ||||
| 		found := false | ||||
| 		for _, item := range namespaceList.Items { | ||||
| 			if item.Metadata.Name == ns { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			t.Errorf("Expected '%v' namespace is not in the parsed data from GetServicesByNamespace()", ns) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetServiceList(t *testing.T) { | ||||
| 	// Set up a test http server | ||||
| 	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		fmt.Fprintln(w, serviceListJsonData) | ||||
| 	})) | ||||
| 	defer testServer.Close() | ||||
|  | ||||
| 	// Overwrite URL constructor to access testServer | ||||
| 	makeURL = func(parts []string) string { | ||||
| 		return testServer.URL | ||||
| 	} | ||||
|  | ||||
| 	expectedServices := []string{"kubernetes", "mynginx", "mywebserver"} | ||||
| 	apiConn := NewK8sConnector("") | ||||
| 	serviceList, err := apiConn.GetServiceList() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error from from GetNamespaceList(), instead got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if serviceList == nil { | ||||
| 		t.Errorf("Expected data from GetServiceList(), instead got nil") | ||||
| 	} | ||||
|  | ||||
| 	kind := serviceList.Kind | ||||
| 	if kind != "ServiceList" { | ||||
| 		t.Errorf("Expected data from GetServiceList() to have Kind='ServiceList', instead got Kind='%v'", kind) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure correct number of services found | ||||
| 	expectedCount := len(expectedServices) | ||||
| 	serviceCount := len(serviceList.Items) | ||||
| 	if serviceCount != expectedCount { | ||||
| 		t.Errorf("Expected '%v' services from GetServiceList(), instead found '%v' services", expectedCount, serviceCount) | ||||
| 	} | ||||
|  | ||||
| 	// Check that all expectedServices are found in the parsed data | ||||
| 	for _, s := range expectedServices { | ||||
| 		found := false | ||||
| 		for _, item := range serviceList.Items { | ||||
| 			if item.Metadata.Name == s { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			t.Errorf("Expected '%v' service is not in the parsed data from GetServiceList()", s) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetServicesByNamespace(t *testing.T) { | ||||
| 	// Set up a test http server | ||||
| 	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		fmt.Fprintln(w, serviceListJsonData) | ||||
| 	})) | ||||
| 	defer testServer.Close() | ||||
|  | ||||
| 	// Overwrite URL constructor to access testServer | ||||
| 	makeURL = func(parts []string) string { | ||||
| 		return testServer.URL | ||||
| 	} | ||||
|  | ||||
| 	expectedNamespaces := []string{"default", "demo"} | ||||
| 	apiConn := NewK8sConnector("") | ||||
| 	servicesByNamespace, err := apiConn.GetServicesByNamespace() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error from from GetServicesByNamespace(), instead got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure correct number of namespaces found | ||||
| 	expectedCount := len(expectedNamespaces) | ||||
| 	namespaceCount := len(servicesByNamespace) | ||||
| 	if namespaceCount != expectedCount { | ||||
| 		t.Errorf("Expected '%v' namespaces from GetServicesByNamespace(), instead found '%v' namespaces", expectedCount, namespaceCount) | ||||
| 	} | ||||
|  | ||||
| 	// Check that all expectedNamespaces are found in the parsed data | ||||
| 	for _, ns := range expectedNamespaces { | ||||
| 		_, ok := servicesByNamespace[ns] | ||||
| 		if !ok { | ||||
| 			t.Errorf("Expected '%v' namespace is not in the parsed data from GetServicesByNamespace()", ns) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetResourceList(t *testing.T) { | ||||
| 	// Set up a test http server | ||||
| 	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		fmt.Fprintln(w, resourceListJsonData) | ||||
| 	})) | ||||
| 	defer testServer.Close() | ||||
|  | ||||
| 	// Overwrite URL constructor to access testServer | ||||
| 	makeURL = func(parts []string) string { | ||||
| 		return testServer.URL | ||||
| 	} | ||||
|  | ||||
| 	expectedResources := []string{"bindings", | ||||
| 		"componentstatuses", | ||||
| 		"configmaps", | ||||
| 		"endpoints", | ||||
| 		"events", | ||||
| 		"limitranges", | ||||
| 		"namespaces", | ||||
| 		"namespaces/finalize", | ||||
| 		"namespaces/status", | ||||
| 		"nodes", | ||||
| 		"nodes/proxy", | ||||
| 		"nodes/status", | ||||
| 		"persistentvolumeclaims", | ||||
| 		"persistentvolumeclaims/status", | ||||
| 		"persistentvolumes", | ||||
| 		"persistentvolumes/status", | ||||
| 		"pods", | ||||
| 		"pods/attach", | ||||
| 		"pods/binding", | ||||
| 		"pods/exec", | ||||
| 		"pods/log", | ||||
| 		"pods/portforward", | ||||
| 		"pods/proxy", | ||||
| 		"pods/status", | ||||
| 		"podtemplates", | ||||
| 		"replicationcontrollers", | ||||
| 		"replicationcontrollers/scale", | ||||
| 		"replicationcontrollers/status", | ||||
| 		"resourcequotas", | ||||
| 		"resourcequotas/status", | ||||
| 		"secrets", | ||||
| 		"serviceaccounts", | ||||
| 		"services", | ||||
| 		"services/proxy", | ||||
| 		"services/status", | ||||
| 	} | ||||
| 	apiConn := NewK8sConnector("") | ||||
| 	resourceList, err := apiConn.GetResourceList() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Expected no error from from GetResourceList(), instead got %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if resourceList == nil { | ||||
| 		t.Errorf("Expected data from GetResourceList(), instead got nil") | ||||
| 	} | ||||
|  | ||||
| 	kind := resourceList.Kind | ||||
| 	if kind != "APIResourceList" { | ||||
| 		t.Errorf("Expected data from GetResourceList() to have Kind='ResourceList', instead got Kind='%v'", kind) | ||||
| 	} | ||||
|  | ||||
| 	// Ensure correct number of resources found | ||||
| 	expectedCount := len(expectedResources) | ||||
| 	resourceCount := len(resourceList.Resources) | ||||
| 	if resourceCount != expectedCount { | ||||
| 		t.Errorf("Expected '%v' resources from GetResourceList(), instead found '%v' resources", expectedCount, resourceCount) | ||||
| 	} | ||||
|  | ||||
| 	// Check that all expectedResources are found in the parsed data | ||||
| 	for _, r := range expectedResources { | ||||
| 		found := false | ||||
| 		for _, item := range resourceList.Resources { | ||||
| 			if item.Name == r { | ||||
| 				found = true | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		if !found { | ||||
| 			t.Errorf("Expected '%v' resource is not in the parsed data from GetResourceList()", r) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Sample namespace data for kubernetes with 3 namespaces: | ||||
| // "default", "demo", and "test". | ||||
| const namespaceListJsonData string = `{ | ||||
|   "kind": "NamespaceList", | ||||
|   "apiVersion": "v1", | ||||
|   "metadata": { | ||||
|     "selfLink": "/api/v1/namespaces/", | ||||
|     "resourceVersion": "121279" | ||||
|   }, | ||||
|   "items": [ | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "default", | ||||
|         "selfLink": "/api/v1/namespaces/default", | ||||
|         "uid": "fb1c92d1-2f39-11e6-b9db-0800279930f6", | ||||
|         "resourceVersion": "6", | ||||
|         "creationTimestamp": "2016-06-10T18:34:35Z" | ||||
|       }, | ||||
|       "spec": { | ||||
|         "finalizers": [ | ||||
|           "kubernetes" | ||||
|         ] | ||||
|       }, | ||||
|       "status": { | ||||
|         "phase": "Active" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "demo", | ||||
|         "selfLink": "/api/v1/namespaces/demo", | ||||
|         "uid": "73be8ffd-2f3a-11e6-b9db-0800279930f6", | ||||
|         "resourceVersion": "111", | ||||
|         "creationTimestamp": "2016-06-10T18:37:57Z" | ||||
|       }, | ||||
|       "spec": { | ||||
|         "finalizers": [ | ||||
|           "kubernetes" | ||||
|         ] | ||||
|       }, | ||||
|       "status": { | ||||
|         "phase": "Active" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "test", | ||||
|         "selfLink": "/api/v1/namespaces/test", | ||||
|         "uid": "c0be05fa-3352-11e6-b9db-0800279930f6", | ||||
|         "resourceVersion": "121276", | ||||
|         "creationTimestamp": "2016-06-15T23:41:59Z" | ||||
|       }, | ||||
|       "spec": { | ||||
|         "finalizers": [ | ||||
|           "kubernetes" | ||||
|         ] | ||||
|       }, | ||||
|       "status": { | ||||
|         "phase": "Active" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| }` | ||||
|  | ||||
| // Sample service data for kubernetes with 3 services: | ||||
| //	* "kubernetes" (in "default" namespace) | ||||
| //	* "mynginx" (in "demo" namespace) | ||||
| //	* "webserver" (in "demo" namespace) | ||||
| const serviceListJsonData string = ` | ||||
| { | ||||
|   "kind": "ServiceList", | ||||
|   "apiVersion": "v1", | ||||
|   "metadata": { | ||||
|     "selfLink": "/api/v1/services", | ||||
|     "resourceVersion": "147965" | ||||
|   }, | ||||
|   "items": [ | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "kubernetes", | ||||
|         "namespace": "default", | ||||
|         "selfLink": "/api/v1/namespaces/default/services/kubernetes", | ||||
|         "uid": "fb1cb0d3-2f39-11e6-b9db-0800279930f6", | ||||
|         "resourceVersion": "7", | ||||
|         "creationTimestamp": "2016-06-10T18:34:35Z", | ||||
|         "labels": { | ||||
|           "component": "apiserver", | ||||
|           "provider": "kubernetes" | ||||
|         } | ||||
|       }, | ||||
|       "spec": { | ||||
|         "ports": [ | ||||
|           { | ||||
|             "name": "https", | ||||
|             "protocol": "TCP", | ||||
|             "port": 443, | ||||
|             "targetPort": 443 | ||||
|           } | ||||
|         ], | ||||
|         "clusterIP": "10.0.0.1", | ||||
|         "type": "ClusterIP", | ||||
|         "sessionAffinity": "None" | ||||
|       }, | ||||
|       "status": { | ||||
|         "loadBalancer": {} | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "mynginx", | ||||
|         "namespace": "demo", | ||||
|         "selfLink": "/api/v1/namespaces/demo/services/mynginx", | ||||
|         "uid": "93c117ac-2f3a-11e6-b9db-0800279930f6", | ||||
|         "resourceVersion": "147", | ||||
|         "creationTimestamp": "2016-06-10T18:38:51Z", | ||||
|         "labels": { | ||||
|           "run": "mynginx" | ||||
|         } | ||||
|       }, | ||||
|       "spec": { | ||||
|         "ports": [ | ||||
|           { | ||||
|             "protocol": "TCP", | ||||
|             "port": 80, | ||||
|             "targetPort": 80 | ||||
|           } | ||||
|         ], | ||||
|         "selector": { | ||||
|           "run": "mynginx" | ||||
|         }, | ||||
|         "clusterIP": "10.0.0.132", | ||||
|         "type": "ClusterIP", | ||||
|         "sessionAffinity": "None" | ||||
|       }, | ||||
|       "status": { | ||||
|         "loadBalancer": {} | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "metadata": { | ||||
|         "name": "mywebserver", | ||||
|         "namespace": "demo", | ||||
|         "selfLink": "/api/v1/namespaces/demo/services/mywebserver", | ||||
|         "uid": "aed62187-33e5-11e6-a224-0800279930f6", | ||||
|         "resourceVersion": "138185", | ||||
|         "creationTimestamp": "2016-06-16T17:13:45Z", | ||||
|         "labels": { | ||||
|           "run": "mywebserver" | ||||
|         } | ||||
|       }, | ||||
|       "spec": { | ||||
|         "ports": [ | ||||
|           { | ||||
|             "protocol": "TCP", | ||||
|             "port": 443, | ||||
|             "targetPort": 443 | ||||
|           } | ||||
|         ], | ||||
|         "selector": { | ||||
|           "run": "mywebserver" | ||||
|         }, | ||||
|         "clusterIP": "10.0.0.63", | ||||
|         "type": "ClusterIP", | ||||
|         "sessionAffinity": "None" | ||||
|       }, | ||||
|       "status": { | ||||
|         "loadBalancer": {} | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| ` | ||||
|  | ||||
| // Sample resource data for kubernetes. | ||||
| const resourceListJsonData string = `{ | ||||
|   "kind": "APIResourceList", | ||||
|   "groupVersion": "v1", | ||||
|   "resources": [ | ||||
|     { | ||||
|       "name": "bindings", | ||||
|       "namespaced": true, | ||||
|       "kind": "Binding" | ||||
|     }, | ||||
|     { | ||||
|       "name": "componentstatuses", | ||||
|       "namespaced": false, | ||||
|       "kind": "ComponentStatus" | ||||
|     }, | ||||
|     { | ||||
|       "name": "configmaps", | ||||
|       "namespaced": true, | ||||
|       "kind": "ConfigMap" | ||||
|     }, | ||||
|     { | ||||
|       "name": "endpoints", | ||||
|       "namespaced": true, | ||||
|       "kind": "Endpoints" | ||||
|     }, | ||||
|     { | ||||
|       "name": "events", | ||||
|       "namespaced": true, | ||||
|       "kind": "Event" | ||||
|     }, | ||||
|     { | ||||
|       "name": "limitranges", | ||||
|       "namespaced": true, | ||||
|       "kind": "LimitRange" | ||||
|     }, | ||||
|     { | ||||
|       "name": "namespaces", | ||||
|       "namespaced": false, | ||||
|       "kind": "Namespace" | ||||
|     }, | ||||
|     { | ||||
|       "name": "namespaces/finalize", | ||||
|       "namespaced": false, | ||||
|       "kind": "Namespace" | ||||
|     }, | ||||
|     { | ||||
|       "name": "namespaces/status", | ||||
|       "namespaced": false, | ||||
|       "kind": "Namespace" | ||||
|     }, | ||||
|     { | ||||
|       "name": "nodes", | ||||
|       "namespaced": false, | ||||
|       "kind": "Node" | ||||
|     }, | ||||
|     { | ||||
|       "name": "nodes/proxy", | ||||
|       "namespaced": false, | ||||
|       "kind": "Node" | ||||
|     }, | ||||
|     { | ||||
|       "name": "nodes/status", | ||||
|       "namespaced": false, | ||||
|       "kind": "Node" | ||||
|     }, | ||||
|     { | ||||
|       "name": "persistentvolumeclaims", | ||||
|       "namespaced": true, | ||||
|       "kind": "PersistentVolumeClaim" | ||||
|     }, | ||||
|     { | ||||
|       "name": "persistentvolumeclaims/status", | ||||
|       "namespaced": true, | ||||
|       "kind": "PersistentVolumeClaim" | ||||
|     }, | ||||
|     { | ||||
|       "name": "persistentvolumes", | ||||
|       "namespaced": false, | ||||
|       "kind": "PersistentVolume" | ||||
|     }, | ||||
|     { | ||||
|       "name": "persistentvolumes/status", | ||||
|       "namespaced": false, | ||||
|       "kind": "PersistentVolume" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/attach", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/binding", | ||||
|       "namespaced": true, | ||||
|       "kind": "Binding" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/exec", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/log", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/portforward", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/proxy", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "pods/status", | ||||
|       "namespaced": true, | ||||
|       "kind": "Pod" | ||||
|     }, | ||||
|     { | ||||
|       "name": "podtemplates", | ||||
|       "namespaced": true, | ||||
|       "kind": "PodTemplate" | ||||
|     }, | ||||
|     { | ||||
|       "name": "replicationcontrollers", | ||||
|       "namespaced": true, | ||||
|       "kind": "ReplicationController" | ||||
|     }, | ||||
|     { | ||||
|       "name": "replicationcontrollers/scale", | ||||
|       "namespaced": true, | ||||
|       "kind": "Scale" | ||||
|     }, | ||||
|     { | ||||
|       "name": "replicationcontrollers/status", | ||||
|       "namespaced": true, | ||||
|       "kind": "ReplicationController" | ||||
|     }, | ||||
|     { | ||||
|       "name": "resourcequotas", | ||||
|       "namespaced": true, | ||||
|       "kind": "ResourceQuota" | ||||
|     }, | ||||
|     { | ||||
|       "name": "resourcequotas/status", | ||||
|       "namespaced": true, | ||||
|       "kind": "ResourceQuota" | ||||
|     }, | ||||
|     { | ||||
|       "name": "secrets", | ||||
|       "namespaced": true, | ||||
|       "kind": "Secret" | ||||
|     }, | ||||
|     { | ||||
|       "name": "serviceaccounts", | ||||
|       "namespaced": true, | ||||
|       "kind": "ServiceAccount" | ||||
|     }, | ||||
|     { | ||||
|       "name": "services", | ||||
|       "namespaced": true, | ||||
|       "kind": "Service" | ||||
|     }, | ||||
|     { | ||||
|       "name": "services/proxy", | ||||
|       "namespaced": true, | ||||
|       "kind": "Service" | ||||
|     }, | ||||
|     { | ||||
|       "name": "services/status", | ||||
|       "namespaced": true, | ||||
|       "kind": "Service" | ||||
|     } | ||||
|   ] | ||||
| }` | ||||
| @@ -20,10 +20,6 @@ import ( | ||||
| 	clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	defaultResyncPeriod = 5 * time.Minute | ||||
| ) | ||||
|  | ||||
| type Kubernetes struct { | ||||
| 	Next         middleware.Handler | ||||
| 	Zones        []string | ||||
| @@ -37,7 +33,7 @@ type Kubernetes struct { | ||||
|  | ||||
| func (g *Kubernetes) StartKubeCache() error { | ||||
| 	// For a custom api server or running outside a k8s cluster | ||||
| 	// set URL in env.KUBERNETES_MASTER | ||||
| 	// set URL in env.KUBERNETES_MASTER or set endpoint in Corefile | ||||
| 	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() | ||||
| 	overrides := &clientcmd.ConfigOverrides{} | ||||
| 	if len(g.APIEndpoint) > 0 { | ||||
| @@ -55,6 +51,7 @@ func (g *Kubernetes) StartKubeCache() error { | ||||
| 		log.Printf("[ERROR] Failed to create kubernetes notification controller: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	log.Printf("[debug] Starting kubernetes middleware with k8s API resync period: %s", g.ResyncPeriod) | ||||
| 	g.APIConn = newdnsController(kubeClient, g.ResyncPeriod) | ||||
|  | ||||
| 	go g.APIConn.Run() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user