add service status detection to kubernetes-discovery

pull/6/head
deads2k 2016-12-19 09:28:54 -05:00
parent d6046aab0e
commit 8f1677b7c8
10 changed files with 226 additions and 31 deletions

View File

@ -17,6 +17,24 @@ spec:
- name: kubernetes-discovery
image: kubernetes-discovery:latest
imagePullPolicy: Never
livenessProbe:
failureThreshold: 3
httpGet:
path: /version
port: 443
scheme: HTTPS
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
readinessProbe:
failureThreshold: 3
httpGet:
path: /version
port: 443
scheme: HTTPS
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
args:
- "--proxy-client-cert-file=/var/run/auth-proxy-client/tls.crt"
- "--proxy-client-key-file=/var/run/auth-proxy-client/tls.key"

View File

@ -34,6 +34,9 @@ go_library(
"//pkg/apiserver/filters:go_default_library",
"//pkg/auth/handlers:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/clientset_generated/clientset:go_default_library",
"//pkg/client/informers/informers_generated:go_default_library",
"//pkg/client/listers/core/v1:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/client/transport:go_default_library",
"//pkg/controller:go_default_library",
@ -64,10 +67,12 @@ go_test(
"//cmd/kubernetes-discovery/pkg/apis/apiregistration:go_default_library",
"//cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion:go_default_library",
"//pkg/api:go_default_library",
"//pkg/api/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/apiserver/request:go_default_library",
"//pkg/auth/user:go_default_library",
"//pkg/client/cache:go_default_library",
"//pkg/client/listers/core/v1:go_default_library",
"//pkg/runtime:go_default_library",
"//pkg/util/diff:go_default_library",
"//pkg/util/sets:go_default_library",

View File

@ -25,6 +25,9 @@ import (
"k8s.io/kubernetes/pkg/api/rest"
apiserverfilters "k8s.io/kubernetes/pkg/apiserver/filters"
authhandlers "k8s.io/kubernetes/pkg/auth/handlers"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
kubeinformers "k8s.io/kubernetes/pkg/client/informers/informers_generated"
v1listers "k8s.io/kubernetes/pkg/client/listers/core/v1"
"k8s.io/kubernetes/pkg/genericapiserver"
genericfilters "k8s.io/kubernetes/pkg/genericapiserver/filters"
"k8s.io/kubernetes/pkg/registry/generic"
@ -33,7 +36,7 @@ import (
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apis/apiregistration/v1alpha1"
clientset "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/clientset_generated/clientset"
discoveryclientset "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/clientset_generated/clientset"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/informers"
listers "k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/client/listers/apiregistration/internalversion"
@ -44,7 +47,8 @@ import (
const legacyAPIServiceName = "v1."
type Config struct {
GenericConfig *genericapiserver.Config
GenericConfig *genericapiserver.Config
CoreAPIServerClient kubeclientset.Interface
// ProxyClientCert/Key are the client cert used to identify this proxy. Backing APIServices use
// this to confirm the proxy's identity
@ -73,6 +77,11 @@ type APIDiscoveryServer struct {
// controller state
lister listers.APIServiceLister
// serviceLister is used by the discovery handler to determine whether or not to try to expose the group
serviceLister v1listers.ServiceLister
// endpointsLister is used by the discovery handler to determine whether or not to try to expose the group
endpointsLister v1listers.EndpointsLister
// proxyMux intercepts requests that need to be proxied to backing API servers
proxyMux *http.ServeMux
}
@ -100,16 +109,19 @@ func (c *Config) SkipComplete() completedConfig {
func (c completedConfig) New() (*APIDiscoveryServer, error) {
informerFactory := informers.NewSharedInformerFactory(
internalclientset.NewForConfigOrDie(c.Config.GenericConfig.LoopbackClientConfig),
clientset.NewForConfigOrDie(c.Config.GenericConfig.LoopbackClientConfig),
discoveryclientset.NewForConfigOrDie(c.Config.GenericConfig.LoopbackClientConfig),
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
)
kubeInformers := kubeinformers.NewSharedInformerFactory(nil, c.CoreAPIServerClient, 5*time.Minute)
proxyMux := http.NewServeMux()
// most API servers don't need to do this, but we need a custom handler chain to handle the special /apis handling here
c.Config.GenericConfig.BuildHandlerChainsFunc = (&handlerChainConfig{
informers: informerFactory,
proxyMux: proxyMux,
informers: informerFactory,
proxyMux: proxyMux,
serviceLister: kubeInformers.Core().V1().Services().Lister(),
endpointsLister: kubeInformers.Core().V1().Endpoints().Lister(),
}).handlerChain
genericServer, err := c.Config.GenericConfig.SkipComplete().New() // completion is done in Complete, no need for a second time
@ -124,6 +136,8 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
proxyClientKey: c.ProxyClientKey,
proxyHandlers: map[string]*proxyHandler{},
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
serviceLister: kubeInformers.Core().V1().Services().Lister(),
endpointsLister: kubeInformers.Core().V1().Endpoints().Lister(),
proxyMux: proxyMux,
}
@ -141,6 +155,7 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
s.GenericAPIServer.AddPostStartHook("start-informers", func(context genericapiserver.PostStartHookContext) error {
informerFactory.Start(wait.NeverStop)
kubeInformers.Start(wait.NeverStop)
return nil
})
s.GenericAPIServer.AddPostStartHook("apiservice-registration-controller", func(context genericapiserver.PostStartHookContext) error {
@ -153,8 +168,10 @@ func (c completedConfig) New() (*APIDiscoveryServer, error) {
// handlerChainConfig is the config used to build the custom handler chain for this api server
type handlerChainConfig struct {
informers informers.SharedInformerFactory
proxyMux *http.ServeMux
informers informers.SharedInformerFactory
proxyMux *http.ServeMux
serviceLister v1listers.ServiceLister
endpointsLister v1listers.EndpointsLister
}
// handlerChain is a method to build the handler chain for this API server. We need a custom handler chain so that we
@ -162,7 +179,7 @@ type handlerChainConfig struct {
// the endpoints differently, since we're proxying all groups except for apiregistration.k8s.io.
func (h *handlerChainConfig) handlerChain(apiHandler http.Handler, c *genericapiserver.Config) (secure, insecure http.Handler) {
// add this as a filter so that we never collide with "already registered" failures on `/apis`
handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices())
handler := WithAPIs(apiHandler, h.informers.Apiregistration().InternalVersion().APIServices(), h.serviceLister, h.endpointsLister)
handler = apiserverfilters.WithAuthorization(handler, c.RequestContextMapper, c.Authorizer)
@ -223,8 +240,10 @@ func (s *APIDiscoveryServer) AddAPIService(apiService *apiregistration.APIServic
// it's time to register the group discovery endpoint
groupPath := "/apis/" + apiService.Spec.Group
groupDiscoveryHandler := &apiGroupHandler{
groupName: apiService.Spec.Group,
lister: s.lister,
groupName: apiService.Spec.Group,
lister: s.lister,
serviceLister: s.serviceLister,
endpointsLister: s.endpointsLister,
}
// discovery is protected
s.GenericAPIServer.HandlerContainer.UnlistedRoutes.Handle(groupPath, groupDiscoveryHandler)

View File

@ -24,6 +24,7 @@ import (
apierrors "k8s.io/kubernetes/pkg/api/errors"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/apiserver"
v1listers "k8s.io/kubernetes/pkg/client/listers/core/v1"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/runtime"
@ -34,10 +35,12 @@ import (
)
// WithAPIs adds the handling for /apis and /apis/<group: -apiregistration.k8s.io>.
func WithAPIs(handler http.Handler, informer informers.APIServiceInformer) http.Handler {
func WithAPIs(handler http.Handler, informer informers.APIServiceInformer, serviceLister v1listers.ServiceLister, endpointsLister v1listers.EndpointsLister) http.Handler {
apisHandler := &apisHandler{
lister: informer.Lister(),
delegate: handler,
lister: informer.Lister(),
delegate: handler,
serviceLister: serviceLister,
endpointsLister: endpointsLister,
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
apisHandler.ServeHTTP(w, req)
@ -49,6 +52,9 @@ func WithAPIs(handler http.Handler, informer informers.APIServiceInformer) http.
type apisHandler struct {
lister listers.APIServiceLister
serviceLister v1listers.ServiceLister
endpointsLister v1listers.EndpointsLister
delegate http.Handler
}
@ -95,7 +101,10 @@ func (r *apisHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if len(apiGroupServers[0].Spec.Group) == 0 {
continue
}
discoveryGroupList.Groups = append(discoveryGroupList.Groups, *newDiscoveryAPIGroup(apiGroupServers))
discoveryGroup := convertToDiscoveryAPIGroup(apiGroupServers, r.serviceLister, r.endpointsLister)
if discoveryGroup != nil {
discoveryGroupList.Groups = append(discoveryGroupList.Groups, *discoveryGroup)
}
}
json, err := runtime.Encode(api.Codecs.LegacyCodec(), discoveryGroupList)
@ -108,18 +117,46 @@ func (r *apisHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
func newDiscoveryAPIGroup(apiServices []*apiregistrationapi.APIService) *metav1.APIGroup {
// convertToDiscoveryAPIGroup takes apiservices in a single group and returns a discovery compatible object.
// if none of the services are available, it will return nil.
func convertToDiscoveryAPIGroup(apiServices []*apiregistrationapi.APIService, serviceLister v1listers.ServiceLister, endpointsLister v1listers.EndpointsLister) *metav1.APIGroup {
apiServicesByGroup := apiregistrationapi.SortedByGroup(apiServices)[0]
discoveryGroup := &metav1.APIGroup{
Name: apiServicesByGroup[0].Spec.Group,
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: apiServicesByGroup[0].Spec.Group + "/" + apiServicesByGroup[0].Spec.Version,
Version: apiServicesByGroup[0].Spec.Version,
},
}
var discoveryGroup *metav1.APIGroup
for _, apiService := range apiServicesByGroup {
// skip any API services without actual services
if _, err := serviceLister.Services(apiService.Spec.Service.Namespace).Get(apiService.Spec.Service.Name); err != nil {
continue
}
hasActiveEndpoints := false
endpoints, err := endpointsLister.Endpoints(apiService.Spec.Service.Namespace).Get(apiService.Spec.Service.Name)
// skip any API services without endpoints
if err != nil {
continue
}
for _, subset := range endpoints.Subsets {
if len(subset.Addresses) > 0 {
hasActiveEndpoints = true
break
}
}
if !hasActiveEndpoints {
continue
}
// the first APIService which is valid becomes the default
if discoveryGroup == nil {
discoveryGroup = &metav1.APIGroup{
Name: apiService.Spec.Group,
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: apiService.Spec.Group + "/" + apiService.Spec.Version,
Version: apiService.Spec.Version,
},
}
}
discoveryGroup.Versions = append(discoveryGroup.Versions,
metav1.GroupVersionForDiscovery{
GroupVersion: apiService.Spec.Group + "/" + apiService.Spec.Version,
@ -136,6 +173,9 @@ type apiGroupHandler struct {
groupName string
lister listers.APIServiceLister
serviceLister v1listers.ServiceLister
endpointsLister v1listers.EndpointsLister
}
func (r *apiGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@ -167,7 +207,12 @@ func (r *apiGroupHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
json, err := runtime.Encode(api.Codecs.LegacyCodec(), newDiscoveryAPIGroup(apiServicesForGroup))
discoveryGroup := convertToDiscoveryAPIGroup(apiServicesForGroup, r.serviceLister, r.endpointsLister)
if discoveryGroup == nil {
http.Error(w, "", http.StatusNotFound)
return
}
json, err := runtime.Encode(api.Codecs.LegacyCodec(), discoveryGroup)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -24,8 +24,10 @@ import (
"testing"
"k8s.io/kubernetes/pkg/api"
corev1 "k8s.io/kubernetes/pkg/api/v1"
metav1 "k8s.io/kubernetes/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/client/cache"
v1listers "k8s.io/kubernetes/pkg/client/listers/core/v1"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/diff"
@ -111,6 +113,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "foo",
Version: "v1",
Priority: 10,
@ -119,6 +125,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "bar",
Version: "v1",
Priority: 11,
@ -164,6 +174,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "foo",
Version: "v1",
Priority: 20,
@ -172,6 +186,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v2.bar"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "bar",
Version: "v2",
Priority: 11,
@ -180,6 +198,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v2.foo"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "foo",
Version: "v2",
Priority: 1,
@ -188,6 +210,10 @@ func TestAPIs(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "bar",
Version: "v1",
Priority: 11,
@ -239,14 +265,26 @@ func TestAPIs(t *testing.T) {
for _, tc := range tests {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
serviceIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
endpointsIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
delegate := &delegationHTTPHandler{}
handler := &apisHandler{
lister: listers.NewAPIServiceLister(indexer),
delegate: delegate,
serviceLister: v1listers.NewServiceLister(serviceIndexer),
endpointsLister: v1listers.NewEndpointsLister(endpointsIndexer),
lister: listers.NewAPIServiceLister(indexer),
delegate: delegate,
}
for _, o := range tc.apiservices {
indexer.Add(o)
}
serviceIndexer.Add(&corev1.Service{ObjectMeta: corev1.ObjectMeta{Namespace: "ns", Name: "api"}})
endpointsIndexer.Add(&corev1.Endpoints{
ObjectMeta: corev1.ObjectMeta{Namespace: "ns", Name: "api"},
Subsets: []corev1.EndpointSubset{
{Addresses: []corev1.EndpointAddress{{}}},
},
},
)
server := httptest.NewServer(handler)
defer server.Close()
@ -316,6 +354,10 @@ func TestAPIGroup(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.foo"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "foo",
Version: "v1",
Priority: 20,
@ -324,6 +366,10 @@ func TestAPIGroup(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v2.bar"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "bar",
Version: "v2",
Priority: 11,
@ -332,6 +378,10 @@ func TestAPIGroup(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v2.foo"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "foo",
Version: "v2",
Priority: 1,
@ -340,6 +390,10 @@ func TestAPIGroup(t *testing.T) {
{
ObjectMeta: api.ObjectMeta{Name: "v1.bar"},
Spec: apiregistration.APIServiceSpec{
Service: apiregistration.ServiceReference{
Namespace: "ns",
Name: "api",
},
Group: "bar",
Version: "v1",
Priority: 11,
@ -369,13 +423,25 @@ func TestAPIGroup(t *testing.T) {
for _, tc := range tests {
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
serviceIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
endpointsIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
handler := &apiGroupHandler{
lister: listers.NewAPIServiceLister(indexer),
groupName: "foo",
lister: listers.NewAPIServiceLister(indexer),
serviceLister: v1listers.NewServiceLister(serviceIndexer),
endpointsLister: v1listers.NewEndpointsLister(endpointsIndexer),
groupName: "foo",
}
for _, o := range tc.apiservices {
indexer.Add(o)
}
serviceIndexer.Add(&corev1.Service{ObjectMeta: corev1.ObjectMeta{Namespace: "ns", Name: "api"}})
endpointsIndexer.Add(&corev1.Endpoints{
ObjectMeta: corev1.ObjectMeta{Namespace: "ns", Name: "api"},
Subsets: []corev1.EndpointSubset{
{Addresses: []corev1.EndpointAddress{{}}},
},
},
)
server := httptest.NewServer(handler)
defer server.Close()
@ -386,8 +452,8 @@ func TestAPIGroup(t *testing.T) {
continue
}
if resp.StatusCode != http.StatusOK {
httputil.DumpResponse(resp, true)
t.Errorf("%s", tc.name)
response, _ := httputil.DumpResponse(resp, true)
t.Errorf("%s: %v", tc.name, string(response))
continue
}
bytes, err := ioutil.ReadAll(resp.Body)

View File

@ -16,6 +16,8 @@ go_library(
"//cmd/kubernetes-discovery/pkg/apiserver:go_default_library",
"//cmd/kubernetes-discovery/pkg/legacy:go_default_library",
"//pkg/api:go_default_library",
"//pkg/client/clientset_generated/clientset:go_default_library",
"//pkg/client/restclient:go_default_library",
"//pkg/genericapiserver:go_default_library",
"//pkg/genericapiserver/filters:go_default_library",
"//pkg/genericapiserver/options:go_default_library",

View File

@ -27,6 +27,8 @@ import (
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/apiserver"
"k8s.io/kubernetes/cmd/kubernetes-discovery/pkg/legacy"
"k8s.io/kubernetes/pkg/api"
kubeclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/genericapiserver"
"k8s.io/kubernetes/pkg/genericapiserver/filters"
genericoptions "k8s.io/kubernetes/pkg/genericapiserver/options"
@ -135,9 +137,19 @@ func (o DiscoveryServerOptions) RunDiscoveryServer() error {
return err
}
kubeconfig, err := restclient.InClusterConfig()
if err != nil {
return err
}
coreAPIServerClient, err := kubeclientset.NewForConfig(kubeconfig)
if err != nil {
return err
}
config := apiserver.Config{
GenericConfig: genericAPIServerConfig,
RESTOptionsGetter: &restOptionsFactory{storageConfig: &o.Etcd.StorageConfig},
GenericConfig: genericAPIServerConfig,
RESTOptionsGetter: &restOptionsFactory{storageConfig: &o.Etcd.StorageConfig},
CoreAPIServerClient: coreAPIServerClient,
}
config.ProxyClientCert, err = ioutil.ReadFile(o.ProxyClientCertFile)

View File

@ -59,7 +59,9 @@ function start_discovery {
# grant permission to run delegated authentication and authorization checks
kubectl_core delete clusterrolebinding discovery:system:auth-delegator > /dev/null 2>&1 || true
kubectl_core delete clusterrolebinding discovery:system:kubernetes-discovery > /dev/null 2>&1 || true
kubectl_core create clusterrolebinding discovery:system:auth-delegator --clusterrole=system:auth-delegator --serviceaccount=kube-public:kubernetes-discovery
kubectl_core create clusterrolebinding discovery:system:kubernetes-discovery --clusterrole=system:kubernetes-discovery --serviceaccount=kube-public:kubernetes-discovery
# make sure the resources we're about to create don't exist
kubectl_core -n kube-public delete secret auth-proxy-client serving-etcd serving-discovery discovery-etcd > /dev/null 2>&1 || true

View File

@ -230,6 +230,14 @@ func ClusterRoles() []rbac.ClusterRole {
rbac.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews").RuleOrDie(),
},
},
{
// a role to use for the API registry, summarization, and proxy handling
ObjectMeta: api.ObjectMeta{Name: "system:kubernetes-discovery"},
Rules: []rbac.PolicyRule{
// it needs to see all services so that it knows whether the ones it points to exist or not
rbac.NewRule(Read...).Groups(legacyGroup).Resources("services", "endpoints").RuleOrDie(),
},
},
{
// a role to use for bootstrapping the kube-controller-manager so it can create the shared informers
// service accounts, and secrets that we need to create separate identities for other controllers

View File

@ -469,6 +469,24 @@ items:
verbs:
- list
- watch
- apiVersion: rbac.authorization.k8s.io/v1alpha1
kind: ClusterRole
metadata:
creationTimestamp: null
labels:
kubernetes.io/bootstrapping: rbac-defaults
name: system:kubernetes-discovery
rules:
- apiGroups:
- ""
attributeRestrictions: null
resources:
- endpoints
- services
verbs:
- get
- list
- watch
- apiVersion: rbac.authorization.k8s.io/v1alpha1
kind: ClusterRole
metadata: