k8s.io/kubernetes@v1.29.3/pkg/controller/serviceaccount/tokens_controller_test.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package serviceaccount
    18  
    19  import (
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	"gopkg.in/square/go-jose.v2/jwt"
    25  	v1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/util/dump"
    30  	utilrand "k8s.io/apimachinery/pkg/util/rand"
    31  	"k8s.io/client-go/informers"
    32  	"k8s.io/client-go/kubernetes/fake"
    33  	core "k8s.io/client-go/testing"
    34  	"k8s.io/kubernetes/pkg/controller"
    35  	"k8s.io/kubernetes/test/utils/ktesting"
    36  )
    37  
    38  type testGenerator struct {
    39  	Token string
    40  	Err   error
    41  }
    42  
    43  func (t *testGenerator) GenerateToken(sc *jwt.Claims, pc interface{}) (string, error) {
    44  	return t.Token, t.Err
    45  }
    46  
    47  // emptySecretReferences is used by a service account without any secrets
    48  func emptySecretReferences() []v1.ObjectReference {
    49  	return []v1.ObjectReference{}
    50  }
    51  
    52  // missingSecretReferences is used by a service account that references secrets which do no exist
    53  func missingSecretReferences() []v1.ObjectReference {
    54  	return []v1.ObjectReference{{Name: "missing-secret-1"}}
    55  }
    56  
    57  // regularSecretReferences is used by a service account that references secrets which are not ServiceAccountTokens
    58  func regularSecretReferences() []v1.ObjectReference {
    59  	return []v1.ObjectReference{{Name: "regular-secret-1"}}
    60  }
    61  
    62  // tokenSecretReferences is used by a service account that references a ServiceAccountToken secret
    63  func tokenSecretReferences() []v1.ObjectReference {
    64  	return []v1.ObjectReference{{Name: "token-secret-1"}}
    65  }
    66  
    67  // serviceAccount returns a service account with the given secret refs
    68  func serviceAccount(secretRefs []v1.ObjectReference) *v1.ServiceAccount {
    69  	return &v1.ServiceAccount{
    70  		ObjectMeta: metav1.ObjectMeta{
    71  			Name:            "default",
    72  			UID:             "12345",
    73  			Namespace:       "default",
    74  			ResourceVersion: "1",
    75  		},
    76  		Secrets: secretRefs,
    77  	}
    78  }
    79  
    80  // updatedServiceAccount returns a service account with the resource version modified
    81  func updatedServiceAccount(secretRefs []v1.ObjectReference) *v1.ServiceAccount {
    82  	sa := serviceAccount(secretRefs)
    83  	sa.ResourceVersion = "2"
    84  	return sa
    85  }
    86  
    87  // opaqueSecret returns a persisted non-ServiceAccountToken secret named "regular-secret-1"
    88  func opaqueSecret() *v1.Secret {
    89  	return &v1.Secret{
    90  		ObjectMeta: metav1.ObjectMeta{
    91  			Name:            "regular-secret-1",
    92  			Namespace:       "default",
    93  			UID:             "23456",
    94  			ResourceVersion: "1",
    95  		},
    96  		Type: "Opaque",
    97  		Data: map[string][]byte{
    98  			"mykey": []byte("mydata"),
    99  		},
   100  	}
   101  }
   102  
   103  // serviceAccountTokenSecret returns an existing ServiceAccountToken secret named "token-secret-1"
   104  func serviceAccountTokenSecret() *v1.Secret {
   105  	return &v1.Secret{
   106  		ObjectMeta: metav1.ObjectMeta{
   107  			Name:            "token-secret-1",
   108  			Namespace:       "default",
   109  			UID:             "23456",
   110  			ResourceVersion: "1",
   111  			Annotations: map[string]string{
   112  				v1.ServiceAccountNameKey: "default",
   113  				v1.ServiceAccountUIDKey:  "12345",
   114  			},
   115  		},
   116  		Type: v1.SecretTypeServiceAccountToken,
   117  		Data: map[string][]byte{
   118  			"token":     []byte("ABC"),
   119  			"ca.crt":    []byte("CA Data"),
   120  			"namespace": []byte("default"),
   121  		},
   122  	}
   123  }
   124  
   125  // serviceAccountTokenSecretWithoutTokenData returns an existing ServiceAccountToken secret that lacks token data
   126  func serviceAccountTokenSecretWithoutTokenData() *v1.Secret {
   127  	secret := serviceAccountTokenSecret()
   128  	delete(secret.Data, v1.ServiceAccountTokenKey)
   129  	return secret
   130  }
   131  
   132  // serviceAccountTokenSecretWithoutCAData returns an existing ServiceAccountToken secret that lacks ca data
   133  func serviceAccountTokenSecretWithoutCAData() *v1.Secret {
   134  	secret := serviceAccountTokenSecret()
   135  	delete(secret.Data, v1.ServiceAccountRootCAKey)
   136  	return secret
   137  }
   138  
   139  // serviceAccountTokenSecretWithCAData returns an existing ServiceAccountToken secret with the specified ca data
   140  func serviceAccountTokenSecretWithCAData(data []byte) *v1.Secret {
   141  	secret := serviceAccountTokenSecret()
   142  	secret.Data[v1.ServiceAccountRootCAKey] = data
   143  	return secret
   144  }
   145  
   146  // serviceAccountTokenSecretWithoutNamespaceData returns an existing ServiceAccountToken secret that lacks namespace data
   147  func serviceAccountTokenSecretWithoutNamespaceData() *v1.Secret {
   148  	secret := serviceAccountTokenSecret()
   149  	delete(secret.Data, v1.ServiceAccountNamespaceKey)
   150  	return secret
   151  }
   152  
   153  // serviceAccountTokenSecretWithNamespaceData returns an existing ServiceAccountToken secret with the specified namespace data
   154  func serviceAccountTokenSecretWithNamespaceData(data []byte) *v1.Secret {
   155  	secret := serviceAccountTokenSecret()
   156  	secret.Data[v1.ServiceAccountNamespaceKey] = data
   157  	return secret
   158  }
   159  
   160  type reaction struct {
   161  	verb     string
   162  	resource string
   163  	reactor  func(t *testing.T) core.ReactionFunc
   164  }
   165  
   166  func TestTokenCreation(t *testing.T) {
   167  	testcases := map[string]struct {
   168  		ClientObjects []runtime.Object
   169  
   170  		IsAsync    bool
   171  		MaxRetries int
   172  
   173  		Reactors []reaction
   174  
   175  		ExistingServiceAccount *v1.ServiceAccount
   176  		ExistingSecrets        []*v1.Secret
   177  
   178  		AddedServiceAccount   *v1.ServiceAccount
   179  		UpdatedServiceAccount *v1.ServiceAccount
   180  		DeletedServiceAccount *v1.ServiceAccount
   181  		AddedSecret           *v1.Secret
   182  		AddedSecretLocal      *v1.Secret
   183  		UpdatedSecret         *v1.Secret
   184  		DeletedSecret         *v1.Secret
   185  
   186  		ExpectedActions []core.Action
   187  	}{
   188  		"new serviceaccount with no secrets": {
   189  			ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())},
   190  
   191  			AddedServiceAccount: serviceAccount(emptySecretReferences()),
   192  			ExpectedActions:     []core.Action{},
   193  		},
   194  		"new serviceaccount with missing secrets": {
   195  			ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
   196  
   197  			AddedServiceAccount: serviceAccount(missingSecretReferences()),
   198  			ExpectedActions:     []core.Action{},
   199  		},
   200  		"new serviceaccount with missing secrets and a local secret in the cache": {
   201  			ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
   202  
   203  			AddedServiceAccount: serviceAccount(tokenSecretReferences()),
   204  			AddedSecretLocal:    serviceAccountTokenSecret(),
   205  			ExpectedActions:     []core.Action{},
   206  		},
   207  		"new serviceaccount with non-token secrets": {
   208  			ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()},
   209  
   210  			AddedServiceAccount: serviceAccount(regularSecretReferences()),
   211  			ExpectedActions:     []core.Action{},
   212  		},
   213  		"new serviceaccount with token secrets": {
   214  			ClientObjects:   []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()},
   215  			ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
   216  
   217  			AddedServiceAccount: serviceAccount(tokenSecretReferences()),
   218  			ExpectedActions:     []core.Action{},
   219  		},
   220  		"updated serviceaccount with no secrets": {
   221  			ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())},
   222  
   223  			UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
   224  			ExpectedActions:       []core.Action{},
   225  		},
   226  		"updated serviceaccount with missing secrets": {
   227  			ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
   228  
   229  			UpdatedServiceAccount: serviceAccount(missingSecretReferences()),
   230  			ExpectedActions:       []core.Action{},
   231  		},
   232  		"updated serviceaccount with non-token secrets": {
   233  			ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()},
   234  
   235  			UpdatedServiceAccount: serviceAccount(regularSecretReferences()),
   236  			ExpectedActions:       []core.Action{},
   237  		},
   238  		"updated serviceaccount with token secrets": {
   239  			ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
   240  
   241  			UpdatedServiceAccount: serviceAccount(tokenSecretReferences()),
   242  			ExpectedActions:       []core.Action{},
   243  		},
   244  		"updated serviceaccount with no secrets with resource conflict": {
   245  			ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences())},
   246  			IsAsync:       true,
   247  			MaxRetries:    1,
   248  
   249  			UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
   250  			ExpectedActions:       []core.Action{},
   251  		},
   252  
   253  		"deleted serviceaccount with no secrets": {
   254  			DeletedServiceAccount: serviceAccount(emptySecretReferences()),
   255  			ExpectedActions:       []core.Action{},
   256  		},
   257  		"deleted serviceaccount with missing secrets": {
   258  			DeletedServiceAccount: serviceAccount(missingSecretReferences()),
   259  			ExpectedActions:       []core.Action{},
   260  		},
   261  		"deleted serviceaccount with non-token secrets": {
   262  			ClientObjects: []runtime.Object{opaqueSecret()},
   263  
   264  			DeletedServiceAccount: serviceAccount(regularSecretReferences()),
   265  			ExpectedActions:       []core.Action{},
   266  		},
   267  		"deleted serviceaccount with token secrets": {
   268  			ClientObjects:   []runtime.Object{serviceAccountTokenSecret()},
   269  			ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
   270  
   271  			DeletedServiceAccount: serviceAccount(tokenSecretReferences()),
   272  			ExpectedActions: []core.Action{
   273  				core.NewDeleteActionWithOptions(
   274  					schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
   275  					metav1.NamespaceDefault, "token-secret-1",
   276  					*metav1.NewPreconditionDeleteOptions("23456")),
   277  			},
   278  		},
   279  
   280  		"added secret without serviceaccount": {
   281  			ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
   282  
   283  			AddedSecret: serviceAccountTokenSecret(),
   284  			ExpectedActions: []core.Action{
   285  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
   286  				core.NewDeleteActionWithOptions(
   287  					schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
   288  					metav1.NamespaceDefault, "token-secret-1",
   289  					*metav1.NewPreconditionDeleteOptions("23456")),
   290  			},
   291  		},
   292  		"added secret with serviceaccount": {
   293  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   294  
   295  			AddedSecret:     serviceAccountTokenSecret(),
   296  			ExpectedActions: []core.Action{},
   297  		},
   298  		"added token secret without token data": {
   299  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
   300  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   301  
   302  			AddedSecret: serviceAccountTokenSecretWithoutTokenData(),
   303  			ExpectedActions: []core.Action{
   304  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   305  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   306  			},
   307  		},
   308  		"added token secret without ca data": {
   309  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutCAData()},
   310  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   311  
   312  			AddedSecret: serviceAccountTokenSecretWithoutCAData(),
   313  			ExpectedActions: []core.Action{
   314  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   315  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   316  			},
   317  		},
   318  		"added token secret with mismatched ca data": {
   319  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))},
   320  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   321  
   322  			AddedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")),
   323  			ExpectedActions: []core.Action{
   324  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   325  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   326  			},
   327  		},
   328  		"added token secret without namespace data": {
   329  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()},
   330  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   331  
   332  			AddedSecret: serviceAccountTokenSecretWithoutNamespaceData(),
   333  			ExpectedActions: []core.Action{
   334  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   335  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   336  			},
   337  		},
   338  		"added token secret with custom namespace data": {
   339  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))},
   340  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   341  
   342  			AddedSecret:     serviceAccountTokenSecretWithNamespaceData([]byte("custom")),
   343  			ExpectedActions: []core.Action{
   344  				// no update is performed... the custom namespace is preserved
   345  			},
   346  		},
   347  
   348  		"updated secret without serviceaccount": {
   349  			ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
   350  
   351  			UpdatedSecret: serviceAccountTokenSecret(),
   352  			ExpectedActions: []core.Action{
   353  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
   354  				core.NewDeleteActionWithOptions(
   355  					schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
   356  					metav1.NamespaceDefault, "token-secret-1",
   357  					*metav1.NewPreconditionDeleteOptions("23456")),
   358  			},
   359  		},
   360  		"updated secret with serviceaccount": {
   361  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   362  
   363  			UpdatedSecret:   serviceAccountTokenSecret(),
   364  			ExpectedActions: []core.Action{},
   365  		},
   366  		"updated token secret without token data": {
   367  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
   368  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   369  
   370  			UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(),
   371  			ExpectedActions: []core.Action{
   372  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   373  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   374  			},
   375  		},
   376  		"updated token secret without ca data": {
   377  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutCAData()},
   378  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   379  
   380  			UpdatedSecret: serviceAccountTokenSecretWithoutCAData(),
   381  			ExpectedActions: []core.Action{
   382  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   383  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   384  			},
   385  		},
   386  		"updated token secret with mismatched ca data": {
   387  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))},
   388  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   389  
   390  			UpdatedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")),
   391  			ExpectedActions: []core.Action{
   392  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   393  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   394  			},
   395  		},
   396  		"updated token secret without namespace data": {
   397  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()},
   398  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   399  
   400  			UpdatedSecret: serviceAccountTokenSecretWithoutNamespaceData(),
   401  			ExpectedActions: []core.Action{
   402  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
   403  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
   404  			},
   405  		},
   406  		"updated token secret with custom namespace data": {
   407  			ClientObjects:          []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))},
   408  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   409  
   410  			UpdatedSecret:   serviceAccountTokenSecretWithNamespaceData([]byte("custom")),
   411  			ExpectedActions: []core.Action{
   412  				// no update is performed... the custom namespace is preserved
   413  			},
   414  		},
   415  
   416  		"deleted secret without serviceaccount": {
   417  			DeletedSecret:   serviceAccountTokenSecret(),
   418  			ExpectedActions: []core.Action{},
   419  		},
   420  		"deleted secret with serviceaccount with reference": {
   421  			ClientObjects:          []runtime.Object{serviceAccount(tokenSecretReferences())},
   422  			ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
   423  
   424  			DeletedSecret: serviceAccountTokenSecret(),
   425  			ExpectedActions: []core.Action{
   426  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
   427  				core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(emptySecretReferences())),
   428  			},
   429  		},
   430  		"deleted secret with serviceaccount without reference": {
   431  			ExistingServiceAccount: serviceAccount(emptySecretReferences()),
   432  
   433  			DeletedSecret: serviceAccountTokenSecret(),
   434  			ExpectedActions: []core.Action{
   435  				core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
   436  			},
   437  		},
   438  	}
   439  
   440  	for k, tc := range testcases {
   441  		t.Run(k, func(t *testing.T) {
   442  			_, ctx := ktesting.NewTestContext(t)
   443  
   444  			// Re-seed to reset name generation
   445  			utilrand.Seed(1)
   446  
   447  			generator := &testGenerator{Token: "ABC"}
   448  
   449  			client := fake.NewSimpleClientset(tc.ClientObjects...)
   450  			for _, reactor := range tc.Reactors {
   451  				client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactor(t))
   452  			}
   453  			informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
   454  			secretInformer := informers.Core().V1().Secrets().Informer()
   455  			secrets := secretInformer.GetStore()
   456  			serviceAccounts := informers.Core().V1().ServiceAccounts().Informer().GetStore()
   457  			controller, err := NewTokensController(informers.Core().V1().ServiceAccounts(), informers.Core().V1().Secrets(), client, TokensControllerOptions{TokenGenerator: generator, RootCA: []byte("CA Data"), MaxRetries: tc.MaxRetries})
   458  			if err != nil {
   459  				t.Fatalf("error creating Tokens controller: %v", err)
   460  			}
   461  
   462  			if tc.ExistingServiceAccount != nil {
   463  				serviceAccounts.Add(tc.ExistingServiceAccount)
   464  			}
   465  			for _, s := range tc.ExistingSecrets {
   466  				secrets.Add(s)
   467  			}
   468  
   469  			if tc.AddedServiceAccount != nil {
   470  				serviceAccounts.Add(tc.AddedServiceAccount)
   471  				controller.queueServiceAccountSync(tc.AddedServiceAccount)
   472  			}
   473  			if tc.UpdatedServiceAccount != nil {
   474  				serviceAccounts.Add(tc.UpdatedServiceAccount)
   475  				controller.queueServiceAccountUpdateSync(nil, tc.UpdatedServiceAccount)
   476  			}
   477  			if tc.DeletedServiceAccount != nil {
   478  				serviceAccounts.Delete(tc.DeletedServiceAccount)
   479  				controller.queueServiceAccountSync(tc.DeletedServiceAccount)
   480  			}
   481  			if tc.AddedSecret != nil {
   482  				secrets.Add(tc.AddedSecret)
   483  				controller.queueSecretSync(tc.AddedSecret)
   484  			}
   485  			if tc.AddedSecretLocal != nil {
   486  				controller.updatedSecrets.Mutation(tc.AddedSecretLocal)
   487  			}
   488  			if tc.UpdatedSecret != nil {
   489  				secrets.Add(tc.UpdatedSecret)
   490  				controller.queueSecretUpdateSync(nil, tc.UpdatedSecret)
   491  			}
   492  			if tc.DeletedSecret != nil {
   493  				secrets.Delete(tc.DeletedSecret)
   494  				controller.queueSecretSync(tc.DeletedSecret)
   495  			}
   496  
   497  			// This is the longest we'll wait for async tests
   498  			timeout := time.Now().Add(30 * time.Second)
   499  			waitedForAdditionalActions := false
   500  
   501  			for {
   502  				if controller.syncServiceAccountQueue.Len() > 0 {
   503  					controller.syncServiceAccount(ctx)
   504  				}
   505  				if controller.syncSecretQueue.Len() > 0 {
   506  					controller.syncSecret(ctx)
   507  				}
   508  
   509  				// The queues still have things to work on
   510  				if controller.syncServiceAccountQueue.Len() > 0 || controller.syncSecretQueue.Len() > 0 {
   511  					continue
   512  				}
   513  
   514  				// If we expect this test to work asynchronously...
   515  				if tc.IsAsync {
   516  					// if we're still missing expected actions within our test timeout
   517  					if len(client.Actions()) < len(tc.ExpectedActions) && time.Now().Before(timeout) {
   518  						// wait for the expected actions (without hotlooping)
   519  						time.Sleep(time.Millisecond)
   520  						continue
   521  					}
   522  
   523  					// if we exactly match our expected actions, wait a bit to make sure no other additional actions show up
   524  					if len(client.Actions()) == len(tc.ExpectedActions) && !waitedForAdditionalActions {
   525  						time.Sleep(time.Second)
   526  						waitedForAdditionalActions = true
   527  						continue
   528  					}
   529  				}
   530  
   531  				break
   532  			}
   533  
   534  			if controller.syncServiceAccountQueue.Len() > 0 {
   535  				t.Errorf("%s: unexpected items in service account queue: %d", k, controller.syncServiceAccountQueue.Len())
   536  			}
   537  			if controller.syncSecretQueue.Len() > 0 {
   538  				t.Errorf("%s: unexpected items in secret queue: %d", k, controller.syncSecretQueue.Len())
   539  			}
   540  
   541  			actions := client.Actions()
   542  			for i, action := range actions {
   543  				if len(tc.ExpectedActions) < i+1 {
   544  					t.Errorf("%s: %d unexpected actions: %+v", k, len(actions)-len(tc.ExpectedActions), actions[i:])
   545  					break
   546  				}
   547  
   548  				expectedAction := tc.ExpectedActions[i]
   549  				if !reflect.DeepEqual(expectedAction, action) {
   550  					t.Errorf("%s:\nExpected:\n%s\ngot:\n%s", k, dump.Pretty(expectedAction), dump.Pretty(action))
   551  					continue
   552  				}
   553  			}
   554  
   555  			if len(tc.ExpectedActions) > len(actions) {
   556  				t.Errorf("%s: %d additional expected actions", k, len(tc.ExpectedActions)-len(actions))
   557  				for _, a := range tc.ExpectedActions[len(actions):] {
   558  					t.Logf("    %+v", a)
   559  				}
   560  			}
   561  		})
   562  	}
   563  }