diff --git a/cmd/kube-controller-manager/app/controllermanager.go b/cmd/kube-controller-manager/app/controllermanager.go index e7964f7b5b..aa85b25d94 100644 --- a/cmd/kube-controller-manager/app/controllermanager.go +++ b/cmd/kube-controller-manager/app/controllermanager.go @@ -20,6 +20,7 @@ limitations under the License. package app import ( + "fmt" "net" "net/http" "net/http/pprof" @@ -40,6 +41,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" "github.com/GoogleCloudPlatform/kubernetes/pkg/service" + "github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder" @@ -249,6 +251,12 @@ func (s *CMServer) Run(_ []string) error { pvclaimBinder.Run() } + // TODO: generate signed token + tokenGenerator := serviceaccount.TokenGeneratorFunc(func(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) { + return fmt.Sprintf("serviceaccount:%s:%s:%s:%s", serviceAccount.Namespace, serviceAccount.Name, serviceAccount.UID, secret.Name), nil + }) + serviceaccount.NewTokensController(kubeClient, serviceaccount.DefaultTokenControllerOptions(tokenGenerator)).Run() + select {} return nil } diff --git a/pkg/api/types.go b/pkg/api/types.go index f1077b2434..b45547c1ef 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1843,6 +1843,8 @@ const ( ServiceAccountUIDKey = "kubernetes.io/service-account.uid" // ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets ServiceAccountTokenKey = "token" + // ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets + ServiceAccountKubeconfigKey = "kubernetes.kubeconfig" ) type SecretList struct { diff --git a/pkg/controller/framework/controller.go b/pkg/controller/framework/controller.go index 7427c06e0e..8f6f2e8126 100644 --- a/pkg/controller/framework/controller.go +++ b/pkg/controller/framework/controller.go @@ -228,3 +228,69 @@ func NewInformer( } return clientState, New(cfg) } + +// NewIndexerInformer returns a cache.Indexer and a controller for populating the index +// while also providing event notifications. You should only used the returned +// cache.Index for Get/List operations; Add/Modify/Deletes will cause the event +// notifications to be faulty. +// +// Parameters: +// * lw is list and watch functions for the source of the resource you want to +// be informed of. +// * objType is an object of the type that you expect to receive. +// * resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate +// calls, even if nothing changed). Otherwise, re-list will be delayed as +// long as possible (until the upstream source closes the watch or times out, +// or you stop the controller). +// * h is the object you want notifications sent to. +// +func NewIndexerInformer( + lw cache.ListerWatcher, + objType runtime.Object, + resyncPeriod time.Duration, + h ResourceEventHandler, + indexers cache.Indexers, +) (cache.Indexer, *Controller) { + // This will hold the client state, as we know it. + clientState := cache.NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers) + + // This will hold incoming changes. Note how we pass clientState in as a + // KeyLister, that way resync operations will result in the correct set + // of update/delete deltas. + fifo := cache.NewDeltaFIFO(cache.MetaNamespaceKeyFunc, nil, clientState) + + cfg := &Config{ + Queue: fifo, + ListerWatcher: lw, + ObjectType: objType, + FullResyncPeriod: resyncPeriod, + RetryOnError: false, + + Process: func(obj interface{}) error { + // from oldest to newest + for _, d := range obj.(cache.Deltas) { + switch d.Type { + case cache.Sync, cache.Added, cache.Updated: + if old, exists, err := clientState.Get(d.Object); err == nil && exists { + if err := clientState.Update(d.Object); err != nil { + return err + } + h.OnUpdate(old, d.Object) + } else { + if err := clientState.Add(d.Object); err != nil { + return err + } + h.OnAdd(d.Object) + } + case cache.Deleted: + if err := clientState.Delete(d.Object); err != nil { + return err + } + h.OnDelete(d.Object) + } + } + return nil + }, + } + return clientState, New(cfg) +} diff --git a/pkg/namespace/namespace_controller.go b/pkg/namespace/namespace_controller.go index a24b063521..1913457d45 100644 --- a/pkg/namespace/namespace_controller.go +++ b/pkg/namespace/namespace_controller.go @@ -107,6 +107,10 @@ func finalize(kubeClient client.Interface, namespace api.Namespace) (*api.Namesp // deleteAllContent will delete all content known to the system in a namespace func deleteAllContent(kubeClient client.Interface, namespace string) (err error) { + err = deleteServiceAccounts(kubeClient, namespace) + if err != nil { + return err + } err = deleteServices(kubeClient, namespace) if err != nil { return err @@ -217,6 +221,20 @@ func deleteResourceQuotas(kubeClient client.Interface, ns string) error { return nil } +func deleteServiceAccounts(kubeClient client.Interface, ns string) error { + items, err := kubeClient.ServiceAccounts(ns).List(labels.Everything(), fields.Everything()) + if err != nil { + return err + } + for i := range items.Items { + err := kubeClient.ServiceAccounts(ns).Delete(items.Items[i].Name) + if err != nil { + return err + } + } + return nil +} + func deleteServices(kubeClient client.Interface, ns string) error { items, err := kubeClient.Services(ns).List(labels.Everything()) if err != nil { diff --git a/pkg/serviceaccount/doc.go b/pkg/serviceaccount/doc.go new file mode 100644 index 0000000000..b69d1a1215 --- /dev/null +++ b/pkg/serviceaccount/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package serviceaccount provides implementations +// to manage service accounts and service account tokens +package serviceaccount diff --git a/pkg/serviceaccount/tokens_controller.go b/pkg/serviceaccount/tokens_controller.go new file mode 100644 index 0000000000..5c3c3fc9cd --- /dev/null +++ b/pkg/serviceaccount/tokens_controller.go @@ -0,0 +1,442 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serviceaccount + +import ( + "fmt" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" + "github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" + "github.com/golang/glog" +) + +// TokensControllerOptions contains options for the TokensController +type TokensControllerOptions struct { + // TokenGenerator is the generator to use to create new tokens + TokenGenerator TokenGenerator + // ServiceAccountResync is the time.Duration at which to fully re-list service accounts. + // If zero, re-list will be delayed as long as possible + ServiceAccountResync time.Duration + // SecretResync is the time.Duration at which to fully re-list secrets. + // If zero, re-list will be delayed as long as possible + SecretResync time.Duration +} + +// DefaultTokenControllerOptions returns +func DefaultTokenControllerOptions(tokenGenerator TokenGenerator) TokensControllerOptions { + return TokensControllerOptions{TokenGenerator: tokenGenerator} +} + +// NewTokensController returns a new *TokensController. +func NewTokensController(cl client.Interface, options TokensControllerOptions) *TokensController { + e := &TokensController{ + client: cl, + token: options.TokenGenerator, + } + + e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything()) + }, + WatchFunc: func(rv string) (watch.Interface, error) { + return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), rv) + }, + }, + &api.ServiceAccount{}, + options.ServiceAccountResync, + framework.ResourceEventHandlerFuncs{ + AddFunc: e.serviceAccountAdded, + UpdateFunc: e.serviceAccountUpdated, + DeleteFunc: e.serviceAccountDeleted, + }, + cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}, + ) + + tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)}) + e.secrets, e.secretController = framework.NewIndexerInformer( + &cache.ListWatch{ + ListFunc: func() (runtime.Object, error) { + return e.client.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector) + }, + WatchFunc: func(rv string) (watch.Interface, error) { + return e.client.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, rv) + }, + }, + &api.Secret{}, + options.SecretResync, + framework.ResourceEventHandlerFuncs{ + AddFunc: e.secretAdded, + UpdateFunc: e.secretUpdated, + DeleteFunc: e.secretDeleted, + }, + cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}, + ) + + return e +} + +// TokensController manages ServiceAccountToken secrets for ServiceAccount objects +type TokensController struct { + stopChan chan struct{} + + client client.Interface + token TokenGenerator + + serviceAccounts cache.Indexer + secrets cache.Indexer + + // Since we join two objects, we'll watch both of them with controllers. + serviceAccountController *framework.Controller + secretController *framework.Controller +} + +// Runs controller loops and returns immediately +func (e *TokensController) Run() { + if e.stopChan == nil { + e.stopChan = make(chan struct{}) + go e.serviceAccountController.Run(e.stopChan) + go e.secretController.Run(e.stopChan) + } +} + +// Stop gracefully shuts down this controller +func (e *TokensController) Stop() { + if e.stopChan != nil { + close(e.stopChan) + e.stopChan = nil + } +} + +// serviceAccountAdded reacts to a ServiceAccount creation by creating a corresponding ServiceAccountToken Secret +func (e *TokensController) serviceAccountAdded(obj interface{}) { + serviceAccount := obj.(*api.ServiceAccount) + err := e.createSecretIfNeeded(serviceAccount) + if err != nil { + glog.Error(err) + } +} + +// serviceAccountUpdated reacts to a ServiceAccount update (or re-list) by ensuring a corresponding ServiceAccountToken Secret exists +func (e *TokensController) serviceAccountUpdated(oldObj interface{}, newObj interface{}) { + newServiceAccount := newObj.(*api.ServiceAccount) + err := e.createSecretIfNeeded(newServiceAccount) + if err != nil { + glog.Error(err) + } +} + +// serviceAccountDeleted reacts to a ServiceAccount deletion by deleting all corresponding ServiceAccountToken Secrets +func (e *TokensController) serviceAccountDeleted(obj interface{}) { + serviceAccount, ok := obj.(*api.ServiceAccount) + if !ok { + // Unknown type. If we missed a ServiceAccount deletion, the + // corresponding secrets will be cleaned up during the Secret re-list + return + } + secrets, err := e.listTokenSecrets(serviceAccount) + if err != nil { + glog.Error(err) + return + } + for _, secret := range secrets { + if err := e.deleteSecret(secret); err != nil { + glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err) + } + } +} + +// secretAdded reacts to a Secret create by ensuring the referenced ServiceAccount exists, and by adding a token to the secret if needed +func (e *TokensController) secretAdded(obj interface{}) { + secret := obj.(*api.Secret) + serviceAccount, err := e.getServiceAccount(secret) + if err != nil { + glog.Error(err) + return + } + if serviceAccount == nil { + if err := e.deleteSecret(secret); err != nil { + glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err) + } + } else { + e.generateTokenIfNeeded(serviceAccount, secret) + } +} + +// secretUpdated reacts to a Secret update (or re-list) by deleting the secret (if the referenced ServiceAccount does not exist) +func (e *TokensController) secretUpdated(oldObj interface{}, newObj interface{}) { + newSecret := newObj.(*api.Secret) + newServiceAccount, err := e.getServiceAccount(newSecret) + if err != nil { + glog.Error(err) + return + } + if newServiceAccount == nil { + if err := e.deleteSecret(newSecret); err != nil { + glog.Errorf("Error deleting secret %s/%s: %v", newSecret.Namespace, newSecret.Name, err) + } + } else { + e.generateTokenIfNeeded(newServiceAccount, newSecret) + } +} + +// secretDeleted reacts to a Secret being deleted by removing a reference from the corresponding ServiceAccount if needed +func (e *TokensController) secretDeleted(obj interface{}) { + secret, ok := obj.(*api.Secret) + if !ok { + // Unknown type. If we missed a Secret deletion, the corresponding ServiceAccount (if it exists) + // will get a secret recreated (if needed) during the ServiceAccount re-list + return + } + + serviceAccount, err := e.getServiceAccount(secret) + if err != nil { + glog.Error(err) + return + } + if serviceAccount == nil { + return + } + + if _, err := e.removeSecretReferenceIfNeeded(serviceAccount, secret.Name); err != nil { + glog.Error(err) + } +} + +// createSecretIfNeeded makes sure at least one ServiceAccountToken secret exists, and is included in the serviceAccount's Secrets list +func (e *TokensController) createSecretIfNeeded(serviceAccount *api.ServiceAccount) error { + // If the service account references no secrets, short-circuit and create a new one + if len(serviceAccount.Secrets) == 0 { + return e.createSecret(serviceAccount) + } + + // If any existing token secrets are referenced by the service account, return + allSecrets, err := e.listTokenSecrets(serviceAccount) + if err != nil { + return err + } + referencedSecrets := getSecretReferences(serviceAccount) + for _, secret := range allSecrets { + if referencedSecrets.Has(secret.Name) { + return nil + } + } + + // Otherwise create a new token secret + return e.createSecret(serviceAccount) +} + +// createSecret creates a secret of type ServiceAccountToken for the given ServiceAccount +func (e *TokensController) createSecret(serviceAccount *api.ServiceAccount) error { + // Build the secret + secret := &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)), + Namespace: serviceAccount.Namespace, + Annotations: map[string]string{ + api.ServiceAccountNameKey: serviceAccount.Name, + api.ServiceAccountUIDKey: string(serviceAccount.UID), + }, + }, + Type: api.SecretTypeServiceAccountToken, + Data: map[string][]byte{}, + } + + // Generate the token + token, err := e.token.GenerateToken(*serviceAccount, *secret) + if err != nil { + return err + } + secret.Data[api.ServiceAccountTokenKey] = []byte(token) + + // Save the secret + if _, err := e.client.Secrets(serviceAccount.Namespace).Create(secret); err != nil { + return err + } + + // We don't want to update the cache's copy of the service account + // so add the secret to a freshly retrieved copy of the service account + serviceAccounts := e.client.ServiceAccounts(serviceAccount.Namespace) + serviceAccount, err = serviceAccounts.Get(serviceAccount.Name) + if err != nil { + return err + } + serviceAccount.Secrets = append(serviceAccount.Secrets, api.ObjectReference{Name: secret.Name}) + + _, err = serviceAccounts.Update(serviceAccount) + if err != nil { + return err + } + return nil +} + +// generateTokenIfNeeded populates the token data for the given Secret if not already set +func (e *TokensController) generateTokenIfNeeded(serviceAccount *api.ServiceAccount, secret *api.Secret) error { + if secret.Annotations == nil { + secret.Annotations = map[string]string{} + } + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + + tokenData, ok := secret.Data[api.ServiceAccountTokenKey] + if ok && len(tokenData) > 0 { + return nil + } + + // Generate the token + token, err := e.token.GenerateToken(*serviceAccount, *secret) + if err != nil { + return err + } + + // Set the token and annotations + secret.Data[api.ServiceAccountTokenKey] = []byte(token) + secret.Annotations[api.ServiceAccountNameKey] = serviceAccount.Name + secret.Annotations[api.ServiceAccountUIDKey] = string(serviceAccount.UID) + + // Save the secret + if _, err := e.client.Secrets(secret.Namespace).Update(secret); err != nil { + return err + } + return nil +} + +// deleteSecret deletes the given secret +func (e *TokensController) deleteSecret(secret *api.Secret) error { + return e.client.Secrets(secret.Namespace).Delete(secret.Name) +} + +// removeSecretReferenceIfNeeded updates the given ServiceAccount to remove a reference to the given secretName if needed. +// Returns whether an update was performed, and any error that occurred +func (e *TokensController) removeSecretReferenceIfNeeded(serviceAccount *api.ServiceAccount, secretName string) (bool, error) { + // See if the account even referenced the secret + if !getSecretReferences(serviceAccount).Has(secretName) { + return false, nil + } + + // We don't want to update the cache's copy of the service account + // so remove the secret from a freshly retrieved copy of the service account + serviceAccounts := e.client.ServiceAccounts(serviceAccount.Namespace) + serviceAccount, err := serviceAccounts.Get(serviceAccount.Name) + if err != nil { + return false, err + } + + // Double-check to see if the account still references the secret + if !getSecretReferences(serviceAccount).Has(secretName) { + return false, nil + } + + secrets := []api.ObjectReference{} + for _, s := range serviceAccount.Secrets { + if s.Name != secretName { + secrets = append(secrets, s) + } + } + serviceAccount.Secrets = secrets + + _, err = serviceAccounts.Update(serviceAccount) + if err != nil { + return false, err + } + + return true, nil +} + +// getServiceAccount returns the ServiceAccount referenced by the given secret. If the secret is not +// of type ServiceAccountToken, or if the referenced ServiceAccount does not exist, nil is returned +func (e *TokensController) getServiceAccount(secret *api.Secret) (*api.ServiceAccount, error) { + name, uid := serviceAccountNameAndUID(secret) + if len(name) == 0 { + return nil, nil + } + + key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: secret.Namespace}} + namespaceAccounts, err := e.serviceAccounts.Index("namespace", key) + if err != nil { + return nil, err + } + + for _, obj := range namespaceAccounts { + serviceAccount := obj.(*api.ServiceAccount) + if name != serviceAccount.Name { + // Name must match + continue + } + if len(uid) > 0 && uid != string(serviceAccount.UID) { + // If UID is specified, it must match + continue + } + return serviceAccount, nil + } + + return nil, nil +} + +// listTokenSecrets returns a list of all of the ServiceAccountToken secrets that +// reference the given service account's name and uid +func (e *TokensController) listTokenSecrets(serviceAccount *api.ServiceAccount) ([]*api.Secret, error) { + key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}} + namespaceSecrets, err := e.secrets.Index("namespace", key) + if err != nil { + return nil, err + } + + items := []*api.Secret{} + for _, obj := range namespaceSecrets { + secret := obj.(*api.Secret) + name, uid := serviceAccountNameAndUID(secret) + if name != serviceAccount.Name { + // Name must match + continue + } + if len(uid) > 0 && uid != string(serviceAccount.UID) { + // If UID is specified, it must match + continue + } + items = append(items, secret) + } + return items, nil +} + +// serviceAccountNameAndUID is a helper method to get the ServiceAccount Name and UID from the given secret +// Returns "","" if the secret is not a ServiceAccountToken secret +// If the name or uid annotation is missing, "" is returned instead +func serviceAccountNameAndUID(secret *api.Secret) (string, string) { + if secret.Type != api.SecretTypeServiceAccountToken { + return "", "" + } + return secret.Annotations[api.ServiceAccountNameKey], secret.Annotations[api.ServiceAccountUIDKey] +} + +func getSecretReferences(serviceAccount *api.ServiceAccount) util.StringSet { + references := util.NewStringSet() + for _, secret := range serviceAccount.Secrets { + references.Insert(secret.Name) + } + return references +} diff --git a/pkg/serviceaccount/tokens_controller_test.go b/pkg/serviceaccount/tokens_controller_test.go new file mode 100644 index 0000000000..041d52575c --- /dev/null +++ b/pkg/serviceaccount/tokens_controller_test.go @@ -0,0 +1,385 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package serviceaccount + +import ( + "math/rand" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +type testGenerator struct { + GeneratedServiceAccounts []api.ServiceAccount + GeneratedSecrets []api.Secret + Token string + Err error +} + +func (t *testGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) { + t.GeneratedSecrets = append(t.GeneratedSecrets, secret) + t.GeneratedServiceAccounts = append(t.GeneratedServiceAccounts, serviceAccount) + return t.Token, t.Err +} + +// emptySecretReferences is used by a service account without any secrets +func emptySecretReferences() []api.ObjectReference { + return []api.ObjectReference{} +} + +// missingSecretReferences is used by a service account that references secrets which do no exist +func missingSecretReferences() []api.ObjectReference { + return []api.ObjectReference{{Name: "missing-secret-1"}} +} + +// regularSecretReferences is used by a service account that references secrets which are not ServiceAccountTokens +func regularSecretReferences() []api.ObjectReference { + return []api.ObjectReference{{Name: "regular-secret-1"}} +} + +// tokenSecretReferences is used by a service account that references a ServiceAccountToken secret +func tokenSecretReferences() []api.ObjectReference { + return []api.ObjectReference{{Name: "token-secret-1"}} +} + +// addTokenSecretReference adds a reference to the ServiceAccountToken that will be created +func addTokenSecretReference(refs []api.ObjectReference) []api.ObjectReference { + return append(refs, api.ObjectReference{Name: "default-token-fplln"}) +} + +// serviceAccount returns a service account with the given secret refs +func serviceAccount(secretRefs []api.ObjectReference) *api.ServiceAccount { + return &api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Name: "default", + UID: "12345", + Namespace: "default", + ResourceVersion: "1", + }, + Secrets: secretRefs, + } +} + +// opaqueSecret returns a persisted non-ServiceAccountToken secret named "regular-secret-1" +func opaqueSecret() *api.Secret { + return &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: "regular-secret-1", + Namespace: "default", + UID: "23456", + ResourceVersion: "1", + }, + Type: "Opaque", + Data: map[string][]byte{ + "mykey": []byte("mydata"), + }, + } +} + +// createdTokenSecret returns the ServiceAccountToken secret posted when creating a new token secret. +// Named "default-token-fplln", since that is the first generated name after rand.Seed(1) +func createdTokenSecret() *api.Secret { + return &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: "default-token-fplln", + Namespace: "default", + Annotations: map[string]string{ + api.ServiceAccountNameKey: "default", + api.ServiceAccountUIDKey: "12345", + }, + }, + Type: api.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + "token": []byte("ABC"), + }, + } +} + +// serviceAccountTokenSecret returns an existing ServiceAccountToken secret named "token-secret-1" +func serviceAccountTokenSecret() *api.Secret { + return &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: "token-secret-1", + Namespace: "default", + UID: "23456", + ResourceVersion: "1", + Annotations: map[string]string{ + api.ServiceAccountNameKey: "default", + api.ServiceAccountUIDKey: "12345", + }, + }, + Type: api.SecretTypeServiceAccountToken, + Data: map[string][]byte{ + "token": []byte("ABC"), + }, + } +} + +// serviceAccountTokenSecretWithoutTokenData returns an existing ServiceAccountToken secret that lacks token data +func serviceAccountTokenSecretWithoutTokenData() *api.Secret { + secret := serviceAccountTokenSecret() + secret.Data = nil + return secret +} + +func TestTokenCreation(t *testing.T) { + testcases := map[string]struct { + ClientObjects []runtime.Object + + ExistingServiceAccount *api.ServiceAccount + ExistingSecrets []*api.Secret + + AddedServiceAccount *api.ServiceAccount + UpdatedServiceAccount *api.ServiceAccount + DeletedServiceAccount *api.ServiceAccount + AddedSecret *api.Secret + UpdatedSecret *api.Secret + DeletedSecret *api.Secret + + ExpectedActions []testclient.FakeAction + }{ + "new serviceaccount with no secrets": { + ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, + + AddedServiceAccount: serviceAccount(emptySecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))}, + }, + }, + "new serviceaccount with missing secrets": { + ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()}, + + AddedServiceAccount: serviceAccount(missingSecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))}, + }, + }, + "new serviceaccount with non-token secrets": { + ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()}, + + AddedServiceAccount: serviceAccount(regularSecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))}, + }, + }, + "new serviceaccount with token secrets": { + ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()}, + ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()}, + + AddedServiceAccount: serviceAccount(tokenSecretReferences()), + ExpectedActions: []testclient.FakeAction{}, + }, + + "updated serviceaccount with no secrets": { + ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()}, + + UpdatedServiceAccount: serviceAccount(emptySecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))}, + }, + }, + "updated serviceaccount with missing secrets": { + ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()}, + + UpdatedServiceAccount: serviceAccount(missingSecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))}, + }, + }, + "updated serviceaccount with non-token secrets": { + ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()}, + + UpdatedServiceAccount: serviceAccount(regularSecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "create-secret", Value: createdTokenSecret()}, + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))}, + }, + }, + "updated serviceaccount with token secrets": { + ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()}, + + UpdatedServiceAccount: serviceAccount(tokenSecretReferences()), + ExpectedActions: []testclient.FakeAction{}, + }, + + "deleted serviceaccount with no secrets": { + DeletedServiceAccount: serviceAccount(emptySecretReferences()), + ExpectedActions: []testclient.FakeAction{}, + }, + "deleted serviceaccount with missing secrets": { + DeletedServiceAccount: serviceAccount(missingSecretReferences()), + ExpectedActions: []testclient.FakeAction{}, + }, + "deleted serviceaccount with non-token secrets": { + ClientObjects: []runtime.Object{opaqueSecret()}, + + DeletedServiceAccount: serviceAccount(regularSecretReferences()), + ExpectedActions: []testclient.FakeAction{}, + }, + "deleted serviceaccount with token secrets": { + ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, + ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()}, + + DeletedServiceAccount: serviceAccount(tokenSecretReferences()), + ExpectedActions: []testclient.FakeAction{ + {Action: "delete-secret", Value: "token-secret-1"}, + }, + }, + + "added secret without serviceaccount": { + ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, + + AddedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{ + {Action: "delete-secret", Value: "token-secret-1"}, + }, + }, + "added secret with serviceaccount": { + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + AddedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{}, + }, + "added token secret without token data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + AddedSecret: serviceAccountTokenSecretWithoutTokenData(), + ExpectedActions: []testclient.FakeAction{ + {Action: "update-secret", Value: serviceAccountTokenSecret()}, + }, + }, + + "updated secret without serviceaccount": { + ClientObjects: []runtime.Object{serviceAccountTokenSecret()}, + + UpdatedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{ + {Action: "delete-secret", Value: "token-secret-1"}, + }, + }, + "updated secret with serviceaccount": { + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + UpdatedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{}, + }, + "updated token secret without token data": { + ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(), + ExpectedActions: []testclient.FakeAction{ + {Action: "update-secret", Value: serviceAccountTokenSecret()}, + }, + }, + + "deleted secret without serviceaccount": { + DeletedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{}, + }, + "deleted secret with serviceaccount with reference": { + ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences())}, + ExistingServiceAccount: serviceAccount(tokenSecretReferences()), + + DeletedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{ + {Action: "get-serviceaccount", Value: "default"}, + {Action: "update-serviceaccount", Value: serviceAccount(emptySecretReferences())}, + }, + }, + "deleted secret with serviceaccount without reference": { + ExistingServiceAccount: serviceAccount(emptySecretReferences()), + + DeletedSecret: serviceAccountTokenSecret(), + ExpectedActions: []testclient.FakeAction{}, + }, + } + + for k, tc := range testcases { + + // Re-seed to reset name generation + rand.Seed(1) + + generator := &testGenerator{Token: "ABC"} + + client := testclient.NewSimpleFake(tc.ClientObjects...) + + controller := NewTokensController(client, DefaultTokenControllerOptions(generator)) + + if tc.ExistingServiceAccount != nil { + controller.serviceAccounts.Add(tc.ExistingServiceAccount) + } + for _, s := range tc.ExistingSecrets { + controller.secrets.Add(s) + } + + if tc.AddedServiceAccount != nil { + controller.serviceAccountAdded(tc.AddedServiceAccount) + } + if tc.UpdatedServiceAccount != nil { + controller.serviceAccountUpdated(nil, tc.UpdatedServiceAccount) + } + if tc.DeletedServiceAccount != nil { + controller.serviceAccountDeleted(tc.DeletedServiceAccount) + } + if tc.AddedSecret != nil { + controller.secretAdded(tc.AddedSecret) + } + if tc.UpdatedSecret != nil { + controller.secretUpdated(nil, tc.UpdatedSecret) + } + if tc.DeletedSecret != nil { + controller.secretDeleted(tc.DeletedSecret) + } + + for i, action := range client.Actions { + if len(tc.ExpectedActions) < i+1 { + t.Errorf("%s: %d unexpected actions: %+v", k, len(client.Actions)-len(tc.ExpectedActions), client.Actions[i:]) + break + } + + expectedAction := tc.ExpectedActions[i] + if expectedAction.Action != action.Action { + t.Errorf("%s: Expected %s, got %s", k, expectedAction.Action, action.Action) + continue + } + if !reflect.DeepEqual(expectedAction.Value, action.Value) { + t.Errorf("%s: Expected\n\t%#v\ngot\n\t%#v", k, expectedAction.Value, action.Value) + continue + } + } + + if len(tc.ExpectedActions) > len(client.Actions) { + t.Errorf("%s: %d additional expected actions:%+v", k, len(tc.ExpectedActions)-len(client.Actions), tc.ExpectedActions[len(client.Actions):]) + } + } +}