* 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
680 lines
16 KiB
Go
680 lines
16 KiB
Go
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"
|
|
}
|
|
]
|
|
}`
|