ServiceAccountTokens controller

pull/6/head
Jordan Liggitt 2015-04-20 23:25:56 -04:00
parent 53d55f4192
commit 0955808668
7 changed files with 940 additions and 0 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

19
pkg/serviceaccount/doc.go Normal file
View File

@ -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

View File

@ -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
}

View File

@ -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):])
}
}
}