From 59db706a4909f7b1816ac7fbac8c7ca542bdce76 Mon Sep 17 00:00:00 2001 From: Yang Le Date: Mon, 19 Sep 2022 17:31:23 +0800 Subject: [PATCH] add more unit test cases Signed-off-by: Yang Le --- cmd/manager/main.go | 8 +- pkg/addon/agent/controller/token.go | 2 +- pkg/addon/agent/controller/token_test.go | 378 ++++++++++++++++++ pkg/addon/manager/addon_test.go | 112 ++++++ .../manager/ephemeral_identity_controller.go | 14 +- .../ephemeral_identity_controller_test.go | 194 +++++++++ pkg/controllers/event/secret_test.go | 109 +++++ pkg/controllers/event/serviceaccount_test.go | 90 +++++ 8 files changed, 900 insertions(+), 7 deletions(-) create mode 100644 pkg/addon/manager/addon_test.go create mode 100644 pkg/addon/manager/ephemeral_identity_controller_test.go create mode 100644 pkg/controllers/event/secret_test.go create mode 100644 pkg/controllers/event/serviceaccount_test.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index de82f7a..2c58d3d 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -177,10 +177,10 @@ func main() { } if features.FeatureGates.Enabled(features.EphemeralIdentity) { - if err := (&manager.EphemeralIdentityReconciler{ - Cache: mgr.GetCache(), - HubClient: mgr.GetClient(), - }).SetupWithManager(mgr); err != nil { + if err := (manager.NewEphemeralIdentityReconciler( + mgr.GetCache(), + mgr.GetClient(), + )).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to register EphemeralIdentityReconciler") os.Exit(1) } diff --git a/pkg/addon/agent/controller/token.go b/pkg/addon/agent/controller/token.go index 0364838..948f8b1 100644 --- a/pkg/addon/agent/controller/token.go +++ b/pkg/addon/agent/controller/token.go @@ -232,7 +232,7 @@ func (r *TokenReconciler) isSoonExpiring(managed *authv1alpha1.ManagedServiceAcc // check if the token should be refreshed now := metav1.Now() - refreshThreshold := managed.Spec.Rotation.Validity.Duration / 5 * 4 + refreshThreshold := managed.Spec.Rotation.Validity.Duration / 5 * 1 lifetime := managed.Status.ExpirationTimestamp.Sub(now.Time) if lifetime < refreshThreshold { return true, nil diff --git a/pkg/addon/agent/controller/token_test.go b/pkg/addon/agent/controller/token_test.go index 87e9b3b..cb9eaf0 100644 --- a/pkg/addon/agent/controller/token_test.go +++ b/pkg/addon/agent/controller/token_test.go @@ -1,12 +1,228 @@ package controller import ( + "context" + "errors" "testing" + "time" "github.com/stretchr/testify/assert" + authv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + fakekube "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + clienttesting "k8s.io/client-go/testing" + authv1alpha1 "open-cluster-management.io/managed-serviceaccount/api/v1alpha1" + "open-cluster-management.io/managed-serviceaccount/pkg/generated/clientset/versioned/scheme" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +func TestReconcile(t *testing.T) { + clusterName := "cluster1" + msaName := "msa1" + token1 := "token1" + token2 := "token2" + ca1 := "ca1" + ca2 := "ca2" + + cases := []struct { + name string + msa *authv1alpha1.ManagedServiceAccount + sa *corev1.ServiceAccount + secret *corev1.Secret + getError error + newToken string + isExistingTokenInvalid bool + expectedError string + validateFunc func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) + }{ + { + name: "not found", + validateFunc: func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) { + assertActions(t, actions, + "delete", // delete service account + ) + }, + }, + { + name: "error to get msa", + getError: errors.New("internal error"), + expectedError: "fail to get managed serviceaccount: internal error", + }, + { + name: "create token", + sa: newServiceAccount(clusterName, msaName), + msa: newManagedServiceAccount(clusterName, msaName).build(), + newToken: token1, + validateFunc: func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) { + assertActions(t, actions, "create", // create serviceaccount + "create", // create tokenrequest + ) + + assertToken(t, hubClient, clusterName, msaName, token1, ca1) + assertMSAConditions(t, hubClient, clusterName, msaName, []metav1.Condition{ + { + Type: authv1alpha1.ConditionTypeTokenReported, + Status: metav1.ConditionTrue, + }, + { + Type: authv1alpha1.ConditionTypeSecretCreated, + Status: metav1.ConditionTrue, + }, + }) + }, + }, + { + name: "token exists", + sa: newServiceAccount(clusterName, msaName), + secret: newSecret(clusterName, msaName, token1, ca1), + msa: newManagedServiceAccount(clusterName, msaName). + withRotationValidity(500*time.Second). + withTokenSecretRef(msaName, time.Now().Add(300*time.Second)). + build(), + newToken: token1, + validateFunc: func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) { + assertActions(t, actions, "create", // create serviceaccount + "create", // create tokenreview + ) + assertToken(t, hubClient, clusterName, msaName, token1, ca1) + }, + }, + { + name: "refresh expiring token", + sa: newServiceAccount(clusterName, msaName), + secret: newSecret(clusterName, msaName, token1, ca1), + msa: newManagedServiceAccount(clusterName, msaName). + withRotationValidity(500*time.Second). + withTokenSecretRef(msaName, time.Now().Add(80*time.Second)). + build(), + newToken: token2, + validateFunc: func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) { + assertActions(t, actions, "create", // create serviceaccount + "create", // create tokenreview + ) + assertToken(t, hubClient, clusterName, msaName, token2, ca1) + assertMSAConditions(t, hubClient, clusterName, msaName, []metav1.Condition{ + { + Type: authv1alpha1.ConditionTypeTokenReported, + Status: metav1.ConditionTrue, + }, + }) + }, + }, + { + name: "refresh invalid token", + sa: newServiceAccount(clusterName, msaName), + secret: newSecret(clusterName, msaName, token2, ca2), + msa: newManagedServiceAccount(clusterName, msaName). + withRotationValidity(500*time.Second). + withTokenSecretRef(msaName, time.Now().Add(300*time.Second)). + build(), + newToken: token1, + isExistingTokenInvalid: true, + validateFunc: func(t *testing.T, hubClient client.Client, actions []clienttesting.Action) { + assertActions(t, actions, "create", // create serviceaccount + "create", // create tokenreview + "create", // create tokenreview + ) + assertToken(t, hubClient, clusterName, msaName, token1, ca1) + assertMSAConditions(t, hubClient, clusterName, msaName, []metav1.Condition{ + { + Type: authv1alpha1.ConditionTypeTokenReported, + Status: metav1.ConditionTrue, + }, + }) + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // create fake kube client of the managed cluster + objs := []runtime.Object{} + if c.sa != nil { + objs = append(objs, c.sa) + } + fakeKubeClient := fakekube.NewSimpleClientset(objs...) + fakeKubeClient.PrependReactor( + "create", + "serviceaccounts", + func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + if action.GetSubresource() == "token" { + return true, &authv1.TokenRequest{ + Status: authv1.TokenRequestStatus{ + Token: c.newToken, + ExpirationTimestamp: metav1.NewTime(time.Now().Add(500 * time.Second)), + }, + }, nil + } + return false, nil, nil + }, + ) + + fakeKubeClient.PrependReactor( + "create", + "tokenreviews", + func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { + return true, &authv1.TokenReview{ + Status: authv1.TokenReviewStatus{ + Authenticated: !c.isExistingTokenInvalid, + }, + }, nil + }, + ) + + // create fake client of the hub cluster + testscheme := scheme.Scheme + authv1alpha1.AddToScheme(testscheme) + corev1.AddToScheme(testscheme) + objs = []runtime.Object{} + if c.msa != nil { + objs = append(objs, c.msa) + } + if c.secret != nil { + objs = append(objs, c.secret) + } + hubClient := fake.NewFakeClientWithScheme(testscheme, objs...) + + reconciler := TokenReconciler{ + Cache: &fakeCache{ + msa: c.msa, + getError: c.getError, + }, + SpokeNativeClient: fakeKubeClient, + HubClient: hubClient, + SpokeClientConfig: &rest.Config{ + TLSClientConfig: rest.TLSClientConfig{ + CAData: []byte(ca1), + }, + }, + } + + _, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{ + Name: msaName, + Namespace: clusterName, + }}) + + if err == nil { + if c.validateFunc != nil { + c.validateFunc(t, hubClient, fakeKubeClient.Actions()) + } + return + } + assert.EqualError(t, err, c.expectedError) + }) + } +} + func TestMergeConditions(t *testing.T) { cases := []struct { name string @@ -85,3 +301,165 @@ func TestMergeConditions(t *testing.T) { } } + +type fakeCache struct { + msa *authv1alpha1.ManagedServiceAccount + getError error +} + +func (f fakeCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if f.getError != nil { + return f.getError + } + + msa, ok := obj.(*authv1alpha1.ManagedServiceAccount) + if !ok { + panic("implement me") + } + + if f.msa == nil { + return apierrors.NewNotFound(schema.GroupResource{ + Group: authv1alpha1.GroupVersion.Group, + Resource: "managedserviceaccounts", + }, key.Name) + } + + f.msa.DeepCopyInto(msa) + return nil +} + +func (f fakeCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + panic("implement me") +} + +func (f fakeCache) GetInformer(ctx context.Context, obj client.Object) (cache.Informer, error) { + panic("implement me") +} + +func (f fakeCache) Start(ctx context.Context) error { + panic("implement me") +} + +func (f fakeCache) WaitForCacheSync(ctx context.Context) bool { + panic("implement me") +} + +func (f fakeCache) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + panic("implement me") +} + +func (f fakeCache) Set(key string, responseBytes []byte) { + panic("implement me") +} + +func (f fakeCache) Delete(key string) { + panic("implement me") +} + +func (f fakeCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) { + panic("implement me") +} + +type managedServiceAccountBuilder struct { + msa *authv1alpha1.ManagedServiceAccount +} + +func newManagedServiceAccount(namespace, name string) *managedServiceAccountBuilder { + return &managedServiceAccountBuilder{ + msa: &authv1alpha1.ManagedServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + } +} + +func (b *managedServiceAccountBuilder) build() *authv1alpha1.ManagedServiceAccount { + return b.msa +} + +func (b *managedServiceAccountBuilder) withRotationValidity(duration time.Duration) *managedServiceAccountBuilder { + b.msa.Spec.Rotation.Validity = metav1.Duration{ + Duration: duration, + } + return b +} + +func (b *managedServiceAccountBuilder) withTokenSecretRef(secretName string, expirationTimestamp time.Time) *managedServiceAccountBuilder { + b.msa.Status.TokenSecretRef = &authv1alpha1.SecretRef{ + Name: secretName, + } + timestamp := metav1.NewTime(expirationTimestamp) + b.msa.Status.ExpirationTimestamp = ×tamp + return b +} + +func newSecret(namespace, name, token, ca string) *corev1.Secret { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: map[string][]byte{}, + } + if len(token) != 0 { + secret.Data[corev1.ServiceAccountTokenKey] = []byte(token) + } + if len(ca) != 0 { + secret.Data[corev1.ServiceAccountRootCAKey] = []byte(ca) + } + return secret +} + +func newServiceAccount(namespace, name string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} + +// assertActions asserts the actual actions have the expected action verb +func assertActions(t *testing.T, actualActions []clienttesting.Action, expectedVerbs ...string) { + if len(actualActions) != len(expectedVerbs) { + t.Fatalf("expected %d call but got: %#v", len(expectedVerbs), actualActions) + } + for i, expected := range expectedVerbs { + if actualActions[i].GetVerb() != expected { + t.Errorf("expected %s action but got: %#v", expected, actualActions[i]) + } + } +} + +func assertToken(t *testing.T, client client.Client, secretNamespace, secretName, token, ca string) { + secret := &corev1.Secret{} + err := client.Get(context.TODO(), types.NamespacedName{ + Namespace: secretNamespace, + Name: secretName, + }, secret) + + assert.NoError(t, err) + tokenData, ok := secret.Data[corev1.ServiceAccountTokenKey] + assert.True(t, ok, "token not exists in secret %s/%s", secretNamespace, secretName) + assert.Equal(t, token, string(tokenData), "unexpected token") + + caData, ok := secret.Data[corev1.ServiceAccountRootCAKey] + assert.True(t, ok, "ca not exists in secret %s/%s", secretNamespace, secretName) + assert.Equal(t, ca, string(caData), "unexpected ca") +} + +func assertMSAConditions(t *testing.T, client client.Client, msaNamespace, msaName string, expected []metav1.Condition) { + msa := &authv1alpha1.ManagedServiceAccount{} + err := client.Get(context.TODO(), types.NamespacedName{ + Namespace: msaNamespace, + Name: msaName, + }, msa) + + assert.NoError(t, err) + for _, condition := range expected { + assert.True(t, meta.IsStatusConditionPresentAndEqual(msa.Status.Conditions, condition.Type, condition.Status), + "condition %q with status %v not found", condition.Type, condition.Status) + } +} diff --git a/pkg/addon/manager/addon_test.go b/pkg/addon/manager/addon_test.go new file mode 100644 index 0000000..da93919 --- /dev/null +++ b/pkg/addon/manager/addon_test.go @@ -0,0 +1,112 @@ +package manager + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakekube "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" + "open-cluster-management.io/addon-framework/pkg/agent" + addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1" + clusterv1 "open-cluster-management.io/api/cluster/v1" + "open-cluster-management.io/managed-serviceaccount/pkg/common" +) + +func TestAddOnAgent(t *testing.T) { + clusterName := "cluster1" + addonName := "addon1" + imageName := "imageName1" + + deployManifests := []string{ + addonName, + "managed-serviceaccount", + "open-cluster-management:managed-serviceaccount:addon-agent", + "open-cluster-management:managed-serviceaccount:addon-agent", + "open-cluster-management:managed-serviceaccount:addon-agent", + "open-cluster-management:managed-serviceaccount:addon-agent", + "managed-serviceaccount-addon-agent", + } + + cases := []struct { + name string + installAll bool + imagePullSecret *corev1.Secret + expectManifests []string + }{ + { + name: "not install all", + expectManifests: deployManifests, + }, + { + name: "install all", + installAll: true, + expectManifests: deployManifests, + }, + { + name: "install all with image pull secret", + installAll: true, + imagePullSecret: &corev1.Secret{}, + expectManifests: append(deployManifests, "open-cluster-management-image-pull-credentials"), + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + fakeKubeClient := fakekube.NewSimpleClientset() + addonAgent := NewManagedServiceAccountAddonAgent(fakeKubeClient, imageName, c.installAll, c.imagePullSecret) + + cluster := &clusterv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + }, + } + addon := &addonv1alpha1.ManagedClusterAddOn{ + ObjectMeta: metav1.ObjectMeta{ + Name: addonName, + Namespace: clusterName, + }, + Spec: addonv1alpha1.ManagedClusterAddOnSpec{ + InstallNamespace: addonName, + }, + } + + // check manifests + manifests, err := addonAgent.Manifests(cluster, addon) + assert.NoError(t, err) + actual := []string{} + for _, manifest := range manifests { + obj, ok := manifest.(metav1.ObjectMetaAccessor) + assert.True(t, ok, "invalid manifest") + if ns := obj.GetObjectMeta().GetNamespace(); len(ns) > 0 { + assert.Equalf(t, addonName, ns, "unexpected ns of manifest %q", obj.GetObjectMeta().GetName()) + } + actual = append(actual, obj.GetObjectMeta().GetName()) + } + assert.ElementsMatch(t, c.expectManifests, actual, "unmatch manifests") + + // check addon options + addonOptions := addonAgent.GetAgentAddonOptions() + assert.NotNil(t, addonOptions.Registration, "registration is not specified") + assert.NotNil(t, addonOptions.Registration.PermissionConfig, "permissionConfig is not specified") + + // check permission config + err = addonOptions.Registration.PermissionConfig(cluster, addon) + assert.NoError(t, err) + actions := fakeKubeClient.Actions() + assert.Len(t, actions, 2) + role := actions[0].(clienttesting.CreateAction).GetObject().(*rbacv1.Role) + assert.Equal(t, clusterName, role.Namespace, "invalid role ns") + assert.Equal(t, "managed-serviceaccount-addon-agent", role.Name, "invalid role name") + rolebinding := actions[1].(clienttesting.CreateAction).GetObject().(*rbacv1.RoleBinding) + assert.Equal(t, clusterName, rolebinding.Namespace, "invalid rolebinding ns") + assert.Equal(t, "managed-serviceaccount-addon-agent", rolebinding.Name, "invalid rolebinding name") + + if c.installAll { + assert.Equal(t, agent.InstallAll, addonOptions.InstallStrategy.Type, "invalid install strategy type") + assert.Equal(t, common.AddonAgentInstallNamespace, addonOptions.InstallStrategy.InstallNamespace, "invalid install ns") + } + }) + } +} diff --git a/pkg/addon/manager/ephemeral_identity_controller.go b/pkg/addon/manager/ephemeral_identity_controller.go index 458ad02..304214d 100644 --- a/pkg/addon/manager/ephemeral_identity_controller.go +++ b/pkg/addon/manager/ephemeral_identity_controller.go @@ -7,6 +7,7 @@ import ( "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/clock" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" @@ -18,9 +19,18 @@ import ( authv1alpha1 "open-cluster-management.io/managed-serviceaccount/api/v1alpha1" ) +func NewEphemeralIdentityReconciler(cache cache.Cache, hubClient client.Client) *EphemeralIdentityReconciler { + return &EphemeralIdentityReconciler{ + Cache: cache, + HubClient: hubClient, + clock: clock.RealClock{}, + } +} + var _ reconcile.Reconciler = &EphemeralIdentityReconciler{} type EphemeralIdentityReconciler struct { + clock clock.Clock cache.Cache HubClient client.Client } @@ -43,7 +53,7 @@ func (r *EphemeralIdentityReconciler) Reconcile(ctx context.Context, request rec managed := &authv1alpha1.ManagedServiceAccount{} if err := r.Cache.Get(ctx, request.NamespacedName, managed); err != nil { if !apierrors.IsNotFound(err) { - return reconcile.Result{}, errors.Wrapf(err, "no such ManagedServiceAccount") + return reconcile.Result{}, errors.Wrapf(err, "fail to get managed serviceaccount") } logger.Info("No such resource") return reconcile.Result{}, nil @@ -54,7 +64,7 @@ func (r *EphemeralIdentityReconciler) Reconcile(ctx context.Context, request rec return reconcile.Result{}, nil } - currentTime := time.Now() + currentTime := r.clock.Now() deletionTime := managed.CreationTimestamp.Add( time.Duration(*managed.Spec.TTLSecondsAfterCreation) * time.Second, ) diff --git a/pkg/addon/manager/ephemeral_identity_controller_test.go b/pkg/addon/manager/ephemeral_identity_controller_test.go new file mode 100644 index 0000000..f3649b7 --- /dev/null +++ b/pkg/addon/manager/ephemeral_identity_controller_test.go @@ -0,0 +1,194 @@ +package manager + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/clock" + authv1alpha1 "open-cluster-management.io/managed-serviceaccount/api/v1alpha1" + "open-cluster-management.io/managed-serviceaccount/pkg/generated/clientset/versioned/scheme" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func TestReconcile(t *testing.T) { + clusterName := "cluster1" + msaName := "msa1" + now := time.Now() + + cases := []struct { + name string + msa *authv1alpha1.ManagedServiceAccount + getError error + expectedResult reconcile.Result + expectedError string + validateFunc func(t *testing.T, hubClient client.Client) + }{ + { + name: "not found", + }, + { + name: "error to get msa", + getError: errors.New("internal error"), + expectedError: "fail to get managed serviceaccount: internal error", + }, + { + name: "without TTLSecondsAfterCreation specified", + msa: newManagedServiceAccount(clusterName, msaName).build(), + expectedError: "fail to get managed serviceaccount: internal error", + }, + { + name: "expired", + msa: newManagedServiceAccount(clusterName, msaName).withTTLSecondsAfterCreation(now.Add(-1000*time.Second), 800).build(), + validateFunc: func(t *testing.T, hubClient client.Client) { + msa := &authv1alpha1.ManagedServiceAccount{} + err := hubClient.Get(context.TODO(), types.NamespacedName{ + Namespace: clusterName, + Name: msaName, + }, msa) + + assert.True(t, apierrors.IsNotFound(err), "msa should have already been deleted") + }, + }, + { + name: "not expired yet", + msa: newManagedServiceAccount(clusterName, msaName).withTTLSecondsAfterCreation(now, 1000).build(), + expectedResult: reconcile.Result{ + Requeue: true, + RequeueAfter: 1000 * time.Second, + }, + validateFunc: func(t *testing.T, hubClient client.Client) { + msa := &authv1alpha1.ManagedServiceAccount{} + err := hubClient.Get(context.TODO(), types.NamespacedName{ + Namespace: clusterName, + Name: msaName, + }, msa) + + assert.NoError(t, err, "unexpected error") + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + // create fake client of the hub cluster + testscheme := scheme.Scheme + authv1alpha1.AddToScheme(testscheme) + + objs := []runtime.Object{} + if c.msa != nil { + objs = append(objs, c.msa) + } + hubClient := fake.NewFakeClientWithScheme(testscheme, objs...) + + reconciler := NewEphemeralIdentityReconciler( + &fakeCache{ + msa: c.msa, + getError: c.getError, + }, hubClient) + reconciler.clock = clock.NewFakeClock(now) + + result, err := reconciler.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{ + Name: msaName, + Namespace: clusterName, + }}) + if err == nil { + assert.Equal(t, c.expectedResult, result, "invalid result") + return + } + assert.EqualError(t, err, c.expectedError) + }) + } +} + +type fakeCache struct { + msa *authv1alpha1.ManagedServiceAccount + getError error +} + +func (f fakeCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if f.getError != nil { + return f.getError + } + + msa, ok := obj.(*authv1alpha1.ManagedServiceAccount) + if !ok { + panic("implement me") + } + + if f.msa == nil { + return apierrors.NewNotFound(schema.GroupResource{ + Group: authv1alpha1.GroupVersion.Group, + Resource: "managedserviceaccounts", + }, key.Name) + } + + f.msa.DeepCopyInto(msa) + return nil +} + +func (f fakeCache) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + panic("implement me") +} + +func (f fakeCache) GetInformer(ctx context.Context, obj client.Object) (cache.Informer, error) { + panic("implement me") +} + +func (f fakeCache) Start(ctx context.Context) error { + panic("implement me") +} + +func (f fakeCache) WaitForCacheSync(ctx context.Context) bool { + panic("implement me") +} + +func (f fakeCache) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + panic("implement me") +} + +func (f fakeCache) Set(key string, responseBytes []byte) { + panic("implement me") +} + +func (f fakeCache) Delete(key string) { + panic("implement me") +} + +func (f fakeCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) { + panic("implement me") +} + +type managedServiceAccountBuilder struct { + msa *authv1alpha1.ManagedServiceAccount +} + +func newManagedServiceAccount(namespace, name string) *managedServiceAccountBuilder { + return &managedServiceAccountBuilder{ + msa: &authv1alpha1.ManagedServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + } +} + +func (b *managedServiceAccountBuilder) build() *authv1alpha1.ManagedServiceAccount { + return b.msa +} + +func (b *managedServiceAccountBuilder) withTTLSecondsAfterCreation(createTime time.Time, ttl int32) *managedServiceAccountBuilder { + b.msa.CreationTimestamp = metav1.NewTime(createTime) + b.msa.Spec.TTLSecondsAfterCreation = &ttl + return b +} diff --git a/pkg/controllers/event/secret_test.go b/pkg/controllers/event/secret_test.go new file mode 100644 index 0000000..227093d --- /dev/null +++ b/pkg/controllers/event/secret_test.go @@ -0,0 +1,109 @@ +package event + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/workqueue" + "open-cluster-management.io/managed-serviceaccount/pkg/common" + "sigs.k8s.io/controller-runtime/pkg/controller/controllertest" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" +) + +func TestSecretEventHandler(t *testing.T) { + secret := &corev1.Secret{} + secretWithLabel := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + common.LabelKeyIsManagedServiceAccount: "true", + }, + }, + } + + cases := []struct { + name string + event interface{} + queued bool + }{ + { + name: "create without label", + event: &event.CreateEvent{ + Object: secret, + }, + }, + { + name: "create with label", + event: &event.CreateEvent{ + Object: secretWithLabel, + }, + queued: true, + }, + { + name: "update without label", + event: &event.UpdateEvent{ + ObjectNew: secret, + }, + }, + { + name: "update with label", + event: &event.UpdateEvent{ + ObjectNew: secretWithLabel, + }, + queued: true, + }, + { + name: "delete without label", + event: &event.DeleteEvent{ + Object: secret, + }, + }, + { + name: "delete with label", + event: &event.DeleteEvent{ + Object: secretWithLabel, + }, + queued: true, + }, + { + name: "generic without label", + event: &event.GenericEvent{ + Object: secret, + }, + }, + { + name: "generic with label", + event: &event.GenericEvent{ + Object: secretWithLabel, + }, + queued: true, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + q := processEvent(NewSecretEventHandler(), c.event) + if c.queued { + assert.Equal(t, 1, q.Len(), "expect event queued") + } else { + assert.Equal(t, 0, q.Len(), "expect event ignored") + } + }) + } +} + +func processEvent(handler handler.EventHandler, evt interface{}) workqueue.RateLimitingInterface { + q := controllertest.Queue{Interface: workqueue.New()} + switch e := evt.(type) { + case *event.CreateEvent: + handler.Create(*e, q) + case *event.UpdateEvent: + handler.Update(*e, q) + case *event.DeleteEvent: + handler.Delete(*e, q) + case *event.GenericEvent: + handler.Generic(*e, q) + } + return q +} diff --git a/pkg/controllers/event/serviceaccount_test.go b/pkg/controllers/event/serviceaccount_test.go new file mode 100644 index 0000000..9b39faf --- /dev/null +++ b/pkg/controllers/event/serviceaccount_test.go @@ -0,0 +1,90 @@ +package event + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "open-cluster-management.io/managed-serviceaccount/pkg/common" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestServiceAccountEventHandler(t *testing.T) { + sa := &corev1.ServiceAccount{} + saWithLabel := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + common.LabelKeyIsManagedServiceAccount: "true", + }, + }, + } + + cases := []struct { + name string + event interface{} + queued bool + }{ + { + name: "create without label", + event: &event.CreateEvent{ + Object: sa, + }, + }, + { + name: "create with label", + event: &event.CreateEvent{ + Object: saWithLabel, + }, + queued: true, + }, + { + name: "update without label", + event: &event.UpdateEvent{ + ObjectNew: sa, + }, + }, + { + name: "update with label", + event: &event.UpdateEvent{ + ObjectNew: saWithLabel, + }, + }, + { + name: "delete without label", + event: &event.DeleteEvent{ + Object: sa, + }, + }, + { + name: "delete with label", + event: &event.DeleteEvent{ + Object: saWithLabel, + }, + queued: true, + }, + { + name: "generic without label", + event: &event.GenericEvent{ + Object: sa, + }, + }, + { + name: "generic with label", + event: &event.GenericEvent{ + Object: saWithLabel, + }, + queued: true, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + q := processEvent(NewServiceAccountEventHandler("cluster1"), c.event) + if c.queued { + assert.Equal(t, 1, q.Len(), "expect event queued") + } else { + assert.Equal(t, 0, q.Len(), "expect event ignored") + } + }) + } +}