mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	k8s middleware cleanup, testcases, basic SRV (#181)
* Removing unnecessary gitignore pattern
* Updating Makefile to run unittests for subpackages
* Adding Corefile validation to ignore overlapping zones
* Fixing SRV query handling
* Updating README.md now that SRV works
* Fixing debug message, adding code comment
* Clarifying implementation of zone normalization
* "Overlapping zones" is ill-defined. Reimplemented zone overlap/subzone
  checking to contain these functions in k8s middleware and provide
  better code comments explaining the normalization.
* Separate build verbosity from test verbosity
* Cleaning up comments to match repo code style
* Merging warning messages into single message
* Moving function docs to before function declaration
* Adding test cases for k8sclient connector
* Tests cover connector create and setting base url
* Fixed bugs in connector create and setting base url functions
* Updaing README to group and order development work
* Priority focused on achieving functional parity with SkyDNS.
* Adding work items to README and cleaning up formatting
* More README format cleaning
* List formating
* Refactoring k8s API call to allow dependency injection
* Add test cases for data parsing from k8s into dataobject structures
* URL is dependency-injected to allow replacement with a mock http
  server during test execution
* Adding more data validation for JSON parsing tests
* Adding test case for GetResourceList()
* Adding notes about SkyDNS embedded IP and port record names
* Marked test case implemented.
* Fixing formatting for example command.
* Fixing formatting
* Adding notes about Docker image building.
* Adding SkyDNS work item
* Updating TODO list
* Adding name template to Corefile to specify how k8s record names are assembled
* Adding template support for multi-segment zones
* Updating example CoreFile for k8s with template comment
* Misc whitespace cleanup
* Adding SkyDNS naming notes
* Adding namespace filtering to CoreFile config
* Updating example k8sCoreFile to specify namespaces
* Removing unused codepath
* Adding check for valid namespace
* More README TODO restructuring to focus effort
* Adding template validation while parsing CoreFile
* Record name template is considered invalid if it contains a symbol of the form ${bar} where the symbol
  "${bar}" is not an accepted template symbol.
* Refactoring generation of answer records
* Parse typeName out of query string
* Refactor answer record creation as operation over list of ServiceItems
* Moving k8s API caching into SkyDNS equivalency segment
* Adding function to assemble record names from template
* Warning: This commit may be broken. Syncing to get laptop code over to dev machine.
* More todo notes
* Adding comment describing sample test data.
* Update k8sCorefile
* Adding comment
* Adding filtering support for kubernetes "type"
* Required refactoring to support reuse of the StringInSlice function.
* Cleaning up formatting
* Adding note about SkyDNS supporting word "any".
* baseUrl -> baseURL
* Also removed debug statement from core/setup/kubernetes.go
* Fixing test breaking from Url -> URL naming changes
* Changing record name template language ${...} -> {...}
* Fix formatting with go fmt
* Updating all k8sclient data getters to return error value
* Adding error message to k8sclient data accessors
* Cleaning up setup for kubernetes
* Removed verbose nils in initial k8s middleware instance
* Set reasonable defaults if CoreFile has no parameters in the
kubernetes block. (k8s endpoint, and name template)
* Formatting cleanup -- go fmt
			
			
This commit is contained in:
		
				
					committed by
					
						 Miek Gieben
						Miek Gieben
					
				
			
			
				
	
			
			
			
						parent
						
							558c34a23e
						
					
				
				
					commit
					289f53d386
				
			| @@ -1,110 +1,113 @@ | ||||
| package k8sclient | ||||
|  | ||||
| import ( | ||||
|     "encoding/json" | ||||
|     "net/http" | ||||
| 	"encoding/json" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
|  | ||||
| func getJson(url string, target interface{}) error { | ||||
|     r, err := http.Get(url) | ||||
|     if err != nil { | ||||
|         return err | ||||
|     } | ||||
|     defer r.Body.Close() | ||||
|  | ||||
|     return json.NewDecoder(r.Body).Decode(target) | ||||
| // 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"` | ||||
| 	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"` | ||||
| 	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"`    | ||||
| 	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"` | ||||
| 	SelfLink        string `json:"selfLink"` | ||||
| 	ResourceVersion string `json:"resourceVersion"` | ||||
| } | ||||
|  | ||||
| type nsItems struct { | ||||
|     Metadata   nsMetadata  `json:"metadata"` | ||||
|     Spec       nsSpec      `json:"spec"` | ||||
|     Status     nsStatus    `json:"status"` | ||||
| 	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"` | ||||
| 	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"` | ||||
| 	Finalizers []string `json:"finalizers"` | ||||
| } | ||||
|  | ||||
| type nsStatus struct { | ||||
|     Phase string `json:"phase"` | ||||
| 	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"`    | ||||
| 	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"` | ||||
| 	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 | ||||
| 	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"` | ||||
| 	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"` | ||||
| 	Name       string `json:"name"` | ||||
| 	Protocol   string `json:"protocol"` | ||||
| 	Port       int    `json:"port"` | ||||
| 	TargetPort int    `json:"targetPort"` | ||||
| } | ||||
|  | ||||
| type serviceStatus struct { | ||||
|     LoadBalancer string `json:"loadBalancer"` | ||||
| 	LoadBalancer string `json:"loadBalancer"` | ||||
| } | ||||
|   | ||||
| @@ -1,117 +1,157 @@ | ||||
| package k8sclient | ||||
|  | ||||
| import ( | ||||
| //    "fmt" | ||||
|     "net/url" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // API strings | ||||
| const ( | ||||
|     apiBase       = "/api/v1" | ||||
|     apiNamespaces = "/namespaces" | ||||
|     apiServices   = "/services" | ||||
| 	apiBase       = "/api/v1" | ||||
| 	apiNamespaces = "/namespaces" | ||||
| 	apiServices   = "/services" | ||||
| ) | ||||
|  | ||||
| // Defaults | ||||
| const ( | ||||
|     defaultBaseUrl = "http://localhost:8080" | ||||
| 	defaultBaseURL = "http://localhost:8080" | ||||
| ) | ||||
|  | ||||
|  | ||||
| type K8sConnector struct { | ||||
|     baseUrl string | ||||
| 	baseURL string | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) SetBaseUrl(u string) error { | ||||
|     validUrl, error := url.Parse(u) | ||||
| func (c *K8sConnector) SetBaseURL(u string) error { | ||||
| 	url, error := url.Parse(u) | ||||
|  | ||||
|     if error != nil { | ||||
|         return error | ||||
|     } | ||||
|     c.baseUrl = validUrl.String() | ||||
| 	if error != nil { | ||||
| 		return error | ||||
| 	} | ||||
|  | ||||
|     return nil | ||||
| 	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 | ||||
| func (c *K8sConnector) GetBaseURL() string { | ||||
| 	return c.baseURL | ||||
| } | ||||
|  | ||||
|  | ||||
| func (c *K8sConnector) GetResourceList() *ResourceList { | ||||
|     resources := new(ResourceList) | ||||
|      | ||||
|     error := getJson((c.baseUrl + apiBase), resources) | ||||
|     if error != nil { | ||||
|         return nil | ||||
|     } | ||||
|  | ||||
|     return resources | ||||
| // 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) | ||||
|  | ||||
| func (c *K8sConnector) GetNamespaceList() *NamespaceList { | ||||
|     namespaces := new(NamespaceList) | ||||
| 	url := makeURL([]string{c.baseURL, apiBase}) | ||||
| 	err := parseJson(url, resources) | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("[ERROR] Response from kubernetes API for GetResourceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
|     error := getJson((c.baseUrl + apiBase + apiNamespaces), namespaces) | ||||
|     if error != nil { | ||||
|         return nil | ||||
|     } | ||||
|  | ||||
|     return namespaces | ||||
| 	return resources, nil | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetNamespaceList() (*NamespaceList, error) { | ||||
| 	namespaces := new(NamespaceList) | ||||
|  | ||||
| func (c *K8sConnector) GetServiceList() *ServiceList { | ||||
|     services := new(ServiceList) | ||||
| 	url := makeURL([]string{c.baseURL, apiBase, apiNamespaces}) | ||||
| 	err := parseJson(url, namespaces) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("[ERROR] Response from kubernetes API for GetNamespaceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
|     error := getJson((c.baseUrl + apiBase + apiServices), services) | ||||
|     if error != nil { | ||||
|         return nil | ||||
|     } | ||||
|  | ||||
|     return services | ||||
| 	return namespaces, nil | ||||
| } | ||||
|  | ||||
| func (c *K8sConnector) GetServiceList() (*ServiceList, error) { | ||||
| 	services := new(ServiceList) | ||||
|  | ||||
| func (c *K8sConnector) GetServicesByNamespace() map[string][]ServiceItem { | ||||
|      // GetServicesByNamespace returns a map of namespacename :: [ kubernetesServiceItem ] | ||||
| 	url := makeURL([]string{c.baseURL, apiBase, apiServices}) | ||||
| 	err := parseJson(url, services) | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("[ERROR] Response from kubernetes API for GetServiceList() is: %v\n", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
|     items := make(map[string][]ServiceItem) | ||||
|  | ||||
|     k8sServiceList := c.GetServiceList() | ||||
|     k8sItemList := k8sServiceList.Items | ||||
|  | ||||
|     for _, i := range k8sItemList { | ||||
|         namespace := i.Metadata.Namespace | ||||
|         items[namespace] = append(items[namespace], i) | ||||
|     } | ||||
|  | ||||
|     return items | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| // GetServicesByNamespace returns a map of | ||||
| // namespacename :: [ kubernetesServiceItem ] | ||||
| func (c *K8sConnector) GetServicesByNamespace() (map[string][]ServiceItem, error) { | ||||
|  | ||||
| func (c *K8sConnector) GetServiceItemInNamespace(namespace string, servicename string) *ServiceItem { | ||||
|     // GetServiceItemInNamespace returns the ServiceItem that matches servicename in the namespace | ||||
| 	items := make(map[string][]ServiceItem) | ||||
|  | ||||
|     itemMap := c.GetServicesByNamespace() | ||||
| 	k8sServiceList, err := c.GetServiceList() | ||||
|  | ||||
|     // TODO: Handle case where namesapce == nil | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("[ERROR] Getting service list produced error: %v", err) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
|     for _, x := range itemMap[namespace] { | ||||
|         if x.Metadata.Name == servicename { | ||||
|             return &x | ||||
|         } | ||||
|     } | ||||
| 	// TODO: handle no response from k8s | ||||
| 	if k8sServiceList == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
|     // No matching item found in namespace | ||||
|     return nil | ||||
| 	k8sItemList := k8sServiceList.Items | ||||
|  | ||||
| 	for _, i := range k8sItemList { | ||||
| 		namespace := i.Metadata.Namespace | ||||
| 		items[namespace] = append(items[namespace], i) | ||||
| 	} | ||||
|  | ||||
| 	return items, nil | ||||
| } | ||||
|  | ||||
| // GetServiceItemsInNamespace returns the ServiceItems that match | ||||
| // servicename in the namespace | ||||
| func (c *K8sConnector) GetServiceItemsInNamespace(namespace string, servicename string) ([]*ServiceItem, error) { | ||||
|  | ||||
| func NewK8sConnector(baseurl string) *K8sConnector { | ||||
|     k := new(K8sConnector) | ||||
|     k.SetBaseUrl(baseurl) | ||||
| 	itemMap, err := c.GetServicesByNamespace() | ||||
|  | ||||
|     return k | ||||
| 	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) | ||||
|  | ||||
| 	if baseURL == "" { | ||||
| 		baseURL = defaultBaseURL | ||||
| 	} | ||||
|  | ||||
| 	err := k.SetBaseURL(baseURL) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	return k | ||||
| } | ||||
|   | ||||
							
								
								
									
										680
									
								
								middleware/kubernetes/k8sclient/k8sclient_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										680
									
								
								middleware/kubernetes/k8sclient/k8sclient_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,680 @@ | ||||
| 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" | ||||
|     } | ||||
|   ] | ||||
| }` | ||||
		Reference in New Issue
	
	Block a user