diff --git a/pkg/oauth/apiserver/apiserver.go b/pkg/oauth/apiserver/apiserver.go index 4e195a2cfc14..95a35e419c43 100644 --- a/pkg/oauth/apiserver/apiserver.go +++ b/pkg/oauth/apiserver/apiserver.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" @@ -121,8 +122,19 @@ func (c *OAuthAPIServerConfig) newV1RESTStorage() (map[string]rest.Storage, erro if err != nil { return nil, err } + coreV1Client, err := corev1.NewForConfig(c.CoreAPIServerClientConfig) + if err != nil { + return nil, err + } - combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(coreClient, coreClient, routeClient, oauthClient.OAuthClients(), saAccountGrantMethod) + combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter( + coreClient, + coreClient, + coreV1Client.Events(""), + routeClient, + oauthClient.OAuthClients(), + saAccountGrantMethod, + ) authorizeTokenStorage, err := authorizetokenetcd.NewREST(c.GenericConfig.RESTOptionsGetter, combinedOAuthClientGetter) if err != nil { return nil, fmt.Errorf("error building REST storage: %v", err) diff --git a/pkg/oauth/apiserver/auth.go b/pkg/oauth/apiserver/auth.go index 9b083eeda7e5..12ef594db822 100644 --- a/pkg/oauth/apiserver/auth.go +++ b/pkg/oauth/apiserver/auth.go @@ -79,7 +79,14 @@ func (c *OAuthServerConfig) WithOAuth(handler http.Handler) (http.Handler, error // pass through all other requests mux.Handle("/", handler) - combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter(c.KubeClient.Core(), c.KubeClient.Core(), c.RouteClient.Route(), c.OAuthClientClient, oauthapi.GrantHandlerType(c.Options.GrantConfig.ServiceAccountMethod)) + combinedOAuthClientGetter := saoauth.NewServiceAccountOAuthClientGetter( + c.KubeClient.Core(), + c.KubeClient.Core(), + c.EventsClient, + c.RouteClient.Route(), + c.OAuthClientClient, + oauthapi.GrantHandlerType(c.Options.GrantConfig.ServiceAccountMethod), + ) errorPageHandler, err := c.getErrorHandler() if err != nil { diff --git a/pkg/oauth/apiserver/oauth_apiserver.go b/pkg/oauth/apiserver/oauth_apiserver.go index e02cb4db7fa4..2468531613ac 100644 --- a/pkg/oauth/apiserver/oauth_apiserver.go +++ b/pkg/oauth/apiserver/oauth_apiserver.go @@ -14,6 +14,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" genericapiserver "k8s.io/apiserver/pkg/server" genericfilters "k8s.io/apiserver/pkg/server/filters" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" kapi "k8s.io/kubernetes/pkg/api" kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" @@ -40,6 +41,9 @@ type OAuthServerConfig struct { // KubeClient is kubeclient with enough permission for the auth API KubeClient kclientset.Interface + // EventsClient is for creating user events + EventsClient corev1.EventInterface + // RouteClient provides a client for OpenShift routes API. RouteClient routeclient.Interface @@ -80,11 +84,16 @@ func NewOAuthServerConfig(oauthConfig configapi.OAuthConfig, userClientConfig *r if err != nil { return nil, err } + eventsClient, err := corev1.NewForConfig(userClientConfig) + if err != nil { + return nil, err + } ret := &OAuthServerConfig{ GenericConfig: genericConfig, Options: oauthConfig, SessionAuth: sessionAuth, + EventsClient: eventsClient.Events(""), IdentityClient: userClient.Identities(), UserClient: userClient.Users(), UserIdentityMappingClient: userClient.UserIdentityMappings(), diff --git a/pkg/serviceaccounts/oauthclient/oauthclientregistry.go b/pkg/serviceaccounts/oauthclient/oauthclientregistry.go index 0e624c95363a..7ecf9c3ea24e 100644 --- a/pkg/serviceaccounts/oauthclient/oauthclientregistry.go +++ b/pkg/serviceaccounts/oauthclient/oauthclientregistry.go @@ -10,7 +10,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + clientv1 "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/tools/record" kapi "k8s.io/kubernetes/pkg/api" kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" "k8s.io/kubernetes/pkg/serviceaccount" @@ -20,7 +25,6 @@ import ( "github.com/openshift/origin/pkg/oauth/registry/oauthclient" routeapi "github.com/openshift/origin/pkg/route/apis/route" routeclient "github.com/openshift/origin/pkg/route/generated/internalclientset/typed/route/internalversion" - "k8s.io/apimachinery/pkg/util/sets" ) const ( @@ -46,8 +50,8 @@ var modelPrefixes = []string{ // namesToObjMapperFunc is linked to a given GroupKind. // Based on the namespace and names provided, it builds a map of resource name to redirect URIs. // The redirect URIs represent the default values as specified by the resource. -// These values can be overridden by user specified data. -type namesToObjMapperFunc func(namespace string, names sets.String) map[string]redirectURIList +// These values can be overridden by user specified data. Errors returned are informative and non-fatal. +type namesToObjMapperFunc func(namespace string, names sets.String) (map[string]redirectURIList, []error) var emptyGroupKind = schema.GroupKind{} // Used with static redirect URIs var routeGroupKind = routeapi.SchemeGroupVersion.WithKind(routeKind).GroupKind() @@ -57,9 +61,10 @@ var legacyRouteGroupKind = routeapi.LegacySchemeGroupVersion.WithKind(routeKind) // var ingressGroupKind = routeapi.SchemeGroupVersion.WithKind(IngressKind).GroupKind() type saOAuthClientAdapter struct { - saClient kcoreclient.ServiceAccountsGetter - secretClient kcoreclient.SecretsGetter - routeClient routeclient.RoutesGetter + saClient kcoreclient.ServiceAccountsGetter + secretClient kcoreclient.SecretsGetter + eventRecorder record.EventRecorder + routeClient routeclient.RoutesGetter // TODO add ingress support //ingressClient ?? @@ -188,22 +193,27 @@ var _ oauthclient.Getter = &saOAuthClientAdapter{} func NewServiceAccountOAuthClientGetter( saClient kcoreclient.ServiceAccountsGetter, secretClient kcoreclient.SecretsGetter, + eventClient corev1.EventInterface, routeClient routeclient.RoutesGetter, delegate oauthclient.Getter, grantMethod oauthapi.GrantHandlerType, ) oauthclient.Getter { - + eventBroadcaster := record.NewBroadcaster() + eventBroadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: eventClient}) + recorder := eventBroadcaster.NewRecorder(kapi.Scheme, clientv1.EventSource{Component: "service-account-oauth-client-getter"}) return &saOAuthClientAdapter{ - saClient: saClient, - secretClient: secretClient, - routeClient: routeClient, - delegate: delegate, - grantMethod: grantMethod, - decoder: kapi.Codecs.UniversalDecoder(), + saClient: saClient, + secretClient: secretClient, + eventRecorder: recorder, + routeClient: routeClient, + delegate: delegate, + grantMethod: grantMethod, + decoder: kapi.Codecs.UniversalDecoder(), } } func (a *saOAuthClientAdapter) Get(name string, options metav1.GetOptions) (*oauthapi.OAuthClient, error) { + var err error saNamespace, saName, err := apiserverserviceaccount.SplitUsername(name) if err != nil { return a.delegate.Get(name, options) @@ -214,17 +224,37 @@ func (a *saOAuthClientAdapter) Get(name string, options metav1.GetOptions) (*oau return nil, err } + var saErrors []error + var failReason string + // Create a warning event combining the collected annotation errors upon failure. + defer func() { + if err != nil && len(saErrors) > 0 && len(failReason) > 0 { + a.eventRecorder.Event(sa, kapi.EventTypeWarning, failReason, utilerrors.NewAggregate(saErrors).Error()) + } + }() + redirectURIs := []string{} - if modelsMap := parseModelsMap(sa.Annotations, a.decoder); len(modelsMap) > 0 { - if uris := a.extractRedirectURIs(modelsMap, saNamespace); len(uris) > 0 { + modelsMap, errs := parseModelsMap(sa.Annotations, a.decoder) + if len(errs) > 0 { + saErrors = append(saErrors, errs...) + } + + if len(modelsMap) > 0 { + uris, extractErrors := a.extractRedirectURIs(modelsMap, saNamespace) + if len(uris) > 0 { redirectURIs = append(redirectURIs, uris.extractValidRedirectURIStrings()...) } + if len(extractErrors) > 0 { + saErrors = append(saErrors, extractErrors...) + } } if len(redirectURIs) == 0 { - return nil, fmt.Errorf( - "%v has no redirectURIs; set %v= or create a dynamic URI using %v=", + err = fmt.Errorf("%v has no redirectURIs; set %v= or create a dynamic URI using %v=", name, OAuthRedirectModelAnnotationURIPrefix, OAuthRedirectModelAnnotationReferencePrefix, ) + failReason = "NoSAOAuthRedirectURIs" + saErrors = append(saErrors, err) + return nil, err } tokens, err := a.getServiceAccountTokens(sa) @@ -232,7 +262,10 @@ func (a *saOAuthClientAdapter) Get(name string, options metav1.GetOptions) (*oau return nil, err } if len(tokens) == 0 { - return nil, fmt.Errorf("%v has no tokens", name) + err = fmt.Errorf("%v has no tokens", name) + failReason = "NoSAOAuthTokens" + saErrors = append(saErrors, err) + return nil, err } saWantsChallenges, _ := strconv.ParseBool(sa.Annotations[OAuthWantChallengesAnnotationPrefix]) @@ -255,9 +288,10 @@ func (a *saOAuthClientAdapter) Get(name string, options metav1.GetOptions) (*oau // parseModelsMap builds a map of model name to model using a service account's annotations. // The model name is only used for building the map (it ties together the uri and reference annotations) -// and serves no functional purpose other than making testing easier. -func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) map[string]model { +// and serves no functional purpose other than making testing easier. Errors returned are informative and non-fatal. +func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) (map[string]model, []error) { models := map[string]model{} + parseErrors := []error{} for key, value := range annotations { prefix, name, ok := parseModelPrefixName(key) if !ok { @@ -268,16 +302,20 @@ func parseModelsMap(annotations map[string]string, decoder runtime.Decoder) map[ case OAuthRedirectModelAnnotationURIPrefix: if u, err := url.Parse(value); err == nil { m.updateFromURI(u) + } else { + parseErrors = append(parseErrors, err) } case OAuthRedirectModelAnnotationReferencePrefix: r := &oauthapi.OAuthRedirectReference{} if err := runtime.DecodeInto(decoder, []byte(value), r); err == nil { m.updateFromReference(&r.Reference) + } else { + parseErrors = append(parseErrors, err) } } models[name] = m } - return models + return models, parseErrors } // parseModelPrefixName determines if the given key is a model prefix. @@ -292,9 +330,10 @@ func parseModelPrefixName(key string) (string, string, bool) { } // extractRedirectURIs builds redirect URIs using the given models and namespace. -// The returned redirect URIs may contain duplicates and invalid entries. -func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, namespace string) redirectURIList { +// The returned redirect URIs may contain duplicates and invalid entries. Errors returned are informative and non-fatal. +func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, namespace string) (redirectURIList, []error) { var data redirectURIList + routeErrors := []error{} groupKindModelListMapper := map[schema.GroupKind]modelList{} // map of GroupKind to all models belonging to it groupKindModelToURI := map[schema.GroupKind]namesToObjMapperFunc{ routeGroupKind: a.redirectURIsFromRoutes, @@ -318,27 +357,37 @@ func (a *saOAuthClientAdapter) extractRedirectURIs(modelsMap map[string]model, n for gk, models := range groupKindModelListMapper { if names := models.getNames(); names.Len() > 0 { - if objMapper := groupKindModelToURI[gk](namespace, names); len(objMapper) > 0 { + objMapper, errs := groupKindModelToURI[gk](namespace, names) + if len(objMapper) > 0 { data = append(data, models.getRedirectURIs(objMapper)...) } + if len(errs) > 0 { + routeErrors = append(routeErrors, errs...) + } } } - return data + return data, routeErrors } // redirectURIsFromRoutes is the namesToObjMapperFunc specific to Routes. // Returns a map of route name to redirect URIs that contain the default data as specified by the route's ingresses. -func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteNames sets.String) map[string]redirectURIList { +// Errors returned are informative and non-fatal. +func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteNames sets.String) (map[string]redirectURIList, []error) { var routes []routeapi.Route + routeErrors := []error{} routeInterface := a.routeClient.Routes(namespace) if osRouteNames.Len() > 1 { if r, err := routeInterface.List(metav1.ListOptions{}); err == nil { routes = r.Items + } else { + routeErrors = append(routeErrors, err) } } else { if r, err := routeInterface.Get(osRouteNames.List()[0], metav1.GetOptions{}); err == nil { routes = append(routes, *r) + } else { + routeErrors = append(routeErrors, err) } } routeMap := map[string]redirectURIList{} @@ -347,7 +396,7 @@ func (a *saOAuthClientAdapter) redirectURIsFromRoutes(namespace string, osRouteN routeMap[route.Name] = redirectURIsFromRoute(&route) } } - return routeMap + return routeMap, routeErrors } // redirectURIsFromRoute returns a list of redirect URIs that contain the default data as specified by the given route's ingresses. diff --git a/pkg/serviceaccounts/oauthclient/oauthclientregistry_test.go b/pkg/serviceaccounts/oauthclient/oauthclientregistry_test.go index c0915a4174e6..e7e8f0800827 100644 --- a/pkg/serviceaccounts/oauthclient/oauthclientregistry_test.go +++ b/pkg/serviceaccounts/oauthclient/oauthclientregistry_test.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" clientgotesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/record" kapi "k8s.io/kubernetes/pkg/api" kapihelper "k8s.io/kubernetes/pkg/api/helper" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" @@ -41,6 +42,7 @@ func TestGetClient(t *testing.T) { expectedDelegation bool expectedErr string + expectedEventMsg string expectedClient *oauthapi.OAuthClient expectedKubeActions []clientgotesting.Action expectedOSActions []clientgotesting.Action @@ -74,8 +76,28 @@ func TestGetClient(t *testing.T) { Annotations: map[string]string{}, }, }), + routeClient: routefake.NewSimpleClientset(), + expectedErr: `system:serviceaccount:ns-01:default has no redirectURIs; set serviceaccounts.openshift.io/oauth-redirecturi.`, + expectedEventMsg: `Warning NoSAOAuthRedirectURIs system:serviceaccount:ns-01:default has no redirectURIs; set serviceaccounts.openshift.io/oauth-redirecturi.= or create a dynamic URI using serviceaccounts.openshift.io/oauth-redirectreference.=`, + + //expectedEventMsg: `Warning NoSAOAuthRedirectURIs [parse ::: missing protocol scheme, system:serviceaccount:ns-01:default has no redirectURIs; set serviceaccounts.openshift.io/oauth-redirecturi.= or create a dynamic URI using serviceaccounts.openshift.io/oauth-redirectreference.=]`, + expectedKubeActions: []clientgotesting.Action{clientgotesting.NewGetAction(serviceAccountsResource, "ns-01", "default")}, + expectedOSActions: []clientgotesting.Action{}, + }, + { + name: "sa invalid redirect scheme", + clientName: "system:serviceaccount:ns-01:default", + kubeClient: fake.NewSimpleClientset( + &kapi.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns-01", + Name: "default", + Annotations: map[string]string{OAuthRedirectModelAnnotationURIPrefix + "incomplete": "::"}, + }, + }), routeClient: routefake.NewSimpleClientset(), expectedErr: `system:serviceaccount:ns-01:default has no redirectURIs; set serviceaccounts.openshift.io/oauth-redirecturi.`, + expectedEventMsg: `Warning NoSAOAuthRedirectURIs [parse ::: missing protocol scheme, system:serviceaccount:ns-01:default has no redirectURIs; set serviceaccounts.openshift.io/oauth-redirecturi.= or create a dynamic URI using serviceaccounts.openshift.io/oauth-redirectreference.=]`, expectedKubeActions: []clientgotesting.Action{clientgotesting.NewGetAction(serviceAccountsResource, "ns-01", "default")}, expectedOSActions: []clientgotesting.Action{}, }, @@ -90,8 +112,9 @@ func TestGetClient(t *testing.T) { Annotations: map[string]string{OAuthRedirectModelAnnotationURIPrefix + "one": "http://anywhere"}, }, }), - routeClient: routefake.NewSimpleClientset(), - expectedErr: `system:serviceaccount:ns-01:default has no tokens`, + routeClient: routefake.NewSimpleClientset(), + expectedErr: `system:serviceaccount:ns-01:default has no tokens`, + expectedEventMsg: `Warning NoSAOAuthTokens system:serviceaccount:ns-01:default has no tokens`, expectedKubeActions: []clientgotesting.Action{ clientgotesting.NewGetAction(serviceAccountsResource, "ns-01", "default"), clientgotesting.NewListAction(secretsResource, secretKind, "ns-01", metav1.ListOptions{}), @@ -258,7 +281,7 @@ func TestGetClient(t *testing.T) { expectedOSActions: []clientgotesting.Action{}, }, { - name: "good SA with a route that don't have a host", + name: "good SA with a route that doesn't have a host", clientName: "system:serviceaccount:ns-01:default", kubeClient: fake.NewSimpleClientset( &kapi.ServiceAccount{ @@ -547,7 +570,16 @@ func TestGetClient(t *testing.T) { for _, tc := range testCases { delegate := &fakeDelegate{} - getter := NewServiceAccountOAuthClientGetter(tc.kubeClient.Core(), tc.kubeClient.Core(), tc.routeClient.Route(), delegate, oauthapi.GrantHandlerPrompt) + fakerecorder := record.NewFakeRecorder(100) + getter := saOAuthClientAdapter{ + saClient: tc.kubeClient.Core(), + secretClient: tc.kubeClient.Core(), + eventRecorder: fakerecorder, + routeClient: tc.routeClient.Route(), + delegate: delegate, + grantMethod: oauthapi.GrantHandlerPrompt, + decoder: kapi.Codecs.UniversalDecoder(), + } client, err := getter.Get(tc.clientName, metav1.GetOptions{}) switch { case len(tc.expectedErr) == 0 && err == nil: @@ -577,8 +609,18 @@ func TestGetClient(t *testing.T) { t.Errorf("%s: expected %#v, got %#v", tc.name, tc.expectedOSActions, tc.routeClient.Actions()) continue } - } + if len(tc.expectedEventMsg) > 0 { + var ev string + select { + case ev = <-fakerecorder.Events: + default: + } + if tc.expectedEventMsg != ev { + t.Errorf("%s: expected event message %#v, got %#v", tc.name, tc.expectedEventMsg, ev) + } + } + } } type fakeDelegate struct { @@ -816,8 +858,12 @@ func TestParseModelsMap(t *testing.T) { }, }, } { - if !reflect.DeepEqual(test.expected, parseModelsMap(test.annotations, decoder)) { - t.Errorf("%s: expected %#v, got %#v", test.name, test.expected, parseModelsMap(test.annotations, decoder)) + models, errs := parseModelsMap(test.annotations, decoder) + if len(errs) > 0 { + t.Errorf("%s: unexpected parseModelsMap errors %v", test.name, errs) + } + if !reflect.DeepEqual(test.expected, models) { + t.Errorf("%s: expected %#v, got %#v", test.name, test.expected, models) } } } @@ -1005,7 +1051,11 @@ func TestGetRedirectURIs(t *testing.T) { }, } { a := buildRouteClient(test.routes) - actual := test.models.getRedirectURIs(a.redirectURIsFromRoutes(test.namespace, test.models.getNames())) + uris, errs := a.redirectURIsFromRoutes(test.namespace, test.models.getNames()) + if len(errs) > 0 { + t.Errorf("%s: unexpected redirectURIsFromRoutes errors %v", test.name, errs) + } + actual := test.models.getRedirectURIs(uris) if !reflect.DeepEqual(test.expected, actual) { t.Errorf("%s: expected %#v, got %#v", test.name, test.expected, actual) } @@ -1172,8 +1222,12 @@ func TestRedirectURIsFromRoutes(t *testing.T) { }, } { a := buildRouteClient(test.routes) - if !reflect.DeepEqual(test.expected, a.redirectURIsFromRoutes(test.namespace, test.names)) { - t.Errorf("%s: expected %#v, got %#v", test.name, test.expected, a.redirectURIsFromRoutes(test.namespace, test.names)) + uris, errs := a.redirectURIsFromRoutes(test.namespace, test.names) + if len(errs) > 0 { + t.Errorf("%s: unexpected redirectURIsFromRoutes errors %v", test.name, errs) + } + if !reflect.DeepEqual(test.expected, uris) { + t.Errorf("%s: expected %#v, got %#v", test.name, test.expected, uris) } } } @@ -1183,7 +1237,10 @@ func buildRouteClient(routes []*routeapi.Route) saOAuthClientAdapter { for _, route := range routes { objects = append(objects, route) } - return saOAuthClientAdapter{routeClient: routefake.NewSimpleClientset(objects...).Route()} + return saOAuthClientAdapter{ + routeClient: routefake.NewSimpleClientset(objects...).Route(), + eventRecorder: record.NewFakeRecorder(100), + } } func buildRedirectObjectReferenceString(kind, name, group string) string { diff --git a/vendor/k8s.io/kubernetes/pkg/printers/internalversion/describe.go b/vendor/k8s.io/kubernetes/pkg/printers/internalversion/describe.go index 0bf823370850..38ce792d4bff 100644 --- a/vendor/k8s.io/kubernetes/pkg/printers/internalversion/describe.go +++ b/vendor/k8s.io/kubernetes/pkg/printers/internalversion/describe.go @@ -2097,10 +2097,15 @@ func (d *ServiceAccountDescriber) Describe(namespace, name string, describerSett } } - return describeServiceAccount(serviceAccount, tokens, missingSecrets) + var events *api.EventList + if describerSettings.ShowEvents { + events, _ = d.Core().Events(namespace).Search(api.Scheme, serviceAccount) + } + + return describeServiceAccount(serviceAccount, tokens, missingSecrets, events) } -func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Secret, missingSecrets sets.String) (string, error) { +func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Secret, missingSecrets sets.String, events *api.EventList) (string, error) { return tabbedString(func(out io.Writer) error { w := NewPrefixWriter(out) w.Write(LEVEL_0, "Name:\t%s\n", serviceAccount.Name) @@ -2152,6 +2157,10 @@ func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Sec w.WriteLine() } + if events != nil { + DescribeEvents(events, w) + } + return nil }) }