diff options
Diffstat (limited to 'middleware/kubernetes/k8sclient')
-rw-r--r-- | middleware/kubernetes/k8sclient/dataobjects.go | 117 | ||||
-rw-r--r-- | middleware/kubernetes/k8sclient/k8sclient.go | 172 | ||||
-rw-r--r-- | middleware/kubernetes/k8sclient/k8sclient_test.go | 680 |
3 files changed, 846 insertions, 123 deletions
diff --git a/middleware/kubernetes/k8sclient/dataobjects.go b/middleware/kubernetes/k8sclient/dataobjects.go index a5ab4f19c..b17adeba4 100644 --- a/middleware/kubernetes/k8sclient/dataobjects.go +++ b/middleware/kubernetes/k8sclient/dataobjects.go @@ -1,110 +1,113 @@ package k8sclient import ( - "encoding/json" - "net/http" + "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 getJson(url string, target interface{}) error { - r, err := http.Get(url) - if err != nil { - return err - } - defer r.Body.Close() +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) + 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"` } diff --git a/middleware/kubernetes/k8sclient/k8sclient.go b/middleware/kubernetes/k8sclient/k8sclient.go index a05ef8905..95300f3b9 100644 --- a/middleware/kubernetes/k8sclient/k8sclient.go +++ b/middleware/kubernetes/k8sclient/k8sclient.go @@ -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 + } - if error != nil { - return error - } - c.baseUrl = validUrl.String() + if !url.IsAbs() { + return errors.New("k8sclient: Kubernetes endpoint url must be an absolute URL") + } - return nil + c.baseURL = url.String() + return nil } -func (c *K8sConnector) GetBaseUrl() string { - return c.baseUrl +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 { - resources := new(ResourceList) - - error := getJson((c.baseUrl + apiBase), resources) - if error != nil { - return nil - } +func (c *K8sConnector) GetResourceList() (*ResourceList, error) { + resources := new(ResourceList) - return resources -} + 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 + } + return resources, nil +} -func (c *K8sConnector) GetNamespaceList() *NamespaceList { - namespaces := new(NamespaceList) +func (c *K8sConnector) GetNamespaceList() (*NamespaceList, error) { + namespaces := new(NamespaceList) - error := getJson((c.baseUrl + apiBase + apiNamespaces), namespaces) - if error != nil { - return nil - } + 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 + } - return namespaces + return namespaces, nil } +func (c *K8sConnector) GetServiceList() (*ServiceList, error) { + services := new(ServiceList) -func (c *K8sConnector) GetServiceList() *ServiceList { - services := new(ServiceList) + 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 + } - error := getJson((c.baseUrl + apiBase + apiServices), services) - if error != nil { - return nil - } - - return services + 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() -func (c *K8sConnector) GetServicesByNamespace() map[string][]ServiceItem { - // GetServicesByNamespace returns a map of namespacename :: [ kubernetesServiceItem ] + if err != nil { + fmt.Printf("[ERROR] Getting service list produced error: %v", err) + return nil, err + } - items := make(map[string][]ServiceItem) + // TODO: handle no response from k8s + if k8sServiceList == nil { + return nil, nil + } - k8sServiceList := c.GetServiceList() - k8sItemList := k8sServiceList.Items + k8sItemList := k8sServiceList.Items - for _, i := range k8sItemList { - namespace := i.Metadata.Namespace - items[namespace] = append(items[namespace], i) - } + for _, i := range k8sItemList { + namespace := i.Metadata.Namespace + items[namespace] = append(items[namespace], i) + } - return items + return items, nil } +// GetServiceItemsInNamespace returns the ServiceItems that match +// servicename in the namespace +func (c *K8sConnector) GetServiceItemsInNamespace(namespace string, servicename string) ([]*ServiceItem, error) { -func (c *K8sConnector) GetServiceItemInNamespace(namespace string, servicename string) *ServiceItem { - // GetServiceItemInNamespace returns the ServiceItem that matches servicename in the namespace + itemMap, err := c.GetServicesByNamespace() - itemMap := c.GetServicesByNamespace() + if err != nil { + fmt.Printf("[ERROR] Getting service list produced error: %v", err) + return nil, err + } - // TODO: Handle case where namesapce == nil + // TODO: Handle case where namespace == nil - for _, x := range itemMap[namespace] { - if x.Metadata.Name == servicename { - return &x - } - } + var serviceItems []*ServiceItem - // No matching item found in namespace - return nil + 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 + } -func NewK8sConnector(baseurl string) *K8sConnector { - k := new(K8sConnector) - k.SetBaseUrl(baseurl) + err := k.SetBaseURL(baseURL) + if err != nil { + return nil + } - return k + return k } diff --git a/middleware/kubernetes/k8sclient/k8sclient_test.go b/middleware/kubernetes/k8sclient/k8sclient_test.go new file mode 100644 index 000000000..eded61b92 --- /dev/null +++ b/middleware/kubernetes/k8sclient/k8sclient_test.go @@ -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" + } + ] +}` |