github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/clusters/multiclustersecret/controller_test.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package multiclustersecret
     5  
     6  import (
     7  	"context"
     8  	"testing"
     9  
    10  	"github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
    11  	"github.com/go-logr/logr"
    12  	"github.com/verrazzano/verrazzano/application-operator/constants"
    13  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    14  
    15  	"github.com/golang/mock/gomock"
    16  	asserts "github.com/stretchr/testify/assert"
    17  	clustersv1alpha1 "github.com/verrazzano/verrazzano/application-operator/apis/clusters/v1alpha1"
    18  	"github.com/verrazzano/verrazzano/application-operator/controllers/clusters"
    19  	clusterstest "github.com/verrazzano/verrazzano/application-operator/controllers/clusters/test"
    20  	"github.com/verrazzano/verrazzano/application-operator/mocks"
    21  	"go.uber.org/zap"
    22  	v1 "k8s.io/api/core/v1"
    23  	"k8s.io/apimachinery/pkg/api/errors"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  )
    30  
    31  const namespace = "unit-mcsecret-namespace"
    32  const crName = "unit-mcsecret"
    33  
    34  // TestReconcilerSetupWithManager test the creation of the Reconciler.
    35  // GIVEN a controller implementation
    36  // WHEN the controller is created
    37  // THEN verify no error is returned
    38  func TestReconcilerSetupWithManager(t *testing.T) {
    39  	assert := asserts.New(t)
    40  
    41  	var mocker *gomock.Controller
    42  	var mgr *mocks.MockManager
    43  	var cli *mocks.MockClient
    44  	var scheme *runtime.Scheme
    45  	var reconciler Reconciler
    46  	var err error
    47  
    48  	mocker = gomock.NewController(t)
    49  	mgr = mocks.NewMockManager(mocker)
    50  	cli = mocks.NewMockClient(mocker)
    51  	scheme = runtime.NewScheme()
    52  	_ = clustersv1alpha1.AddToScheme(scheme)
    53  	reconciler = Reconciler{Client: cli, Scheme: scheme}
    54  	mgr.EXPECT().GetControllerOptions().AnyTimes()
    55  	mgr.EXPECT().GetScheme().Return(scheme)
    56  	mgr.EXPECT().GetLogger().Return(logr.Discard())
    57  	mgr.EXPECT().SetFields(gomock.Any()).Return(nil).AnyTimes()
    58  	mgr.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes()
    59  	err = reconciler.SetupWithManager(mgr)
    60  	mocker.Finish()
    61  	assert.NoError(err)
    62  }
    63  
    64  // TestReconcileCreateSecret tests the basic happy path of reconciling a MultiClusterSecret. We
    65  // expect to write out a K8S secret
    66  // GIVEN a MultiClusterSecret resource is created
    67  // WHEN the controller Reconcile function is called
    68  // THEN expect a Secret to be created
    69  func TestReconcileCreateSecret(t *testing.T) {
    70  	assert := asserts.New(t)
    71  
    72  	mocker := gomock.NewController(t)
    73  	cli := mocks.NewMockClient(mocker)
    74  	mockStatusWriter := mocks.NewMockStatusWriter(mocker)
    75  
    76  	secretData := map[string][]byte{"username": []byte("aaaaa")}
    77  
    78  	mcSecretSample := getSampleMCSecret(namespace, crName, secretData)
    79  
    80  	// expect a call to fetch the MultiClusterSecret
    81  	doExpectGetMultiClusterSecret(cli, mcSecretSample, false)
    82  
    83  	// expect a call to fetch the managed cluster registration secret
    84  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
    85  
    86  	// expect a call to fetch existing corev1.Secret and return not found error, to test create case
    87  	cli.EXPECT().
    88  		Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: crName}, gomock.Not(gomock.Nil()), gomock.Any()).
    89  		Return(errors.NewNotFound(schema.GroupResource{Group: "", Resource: "Secret"}, crName))
    90  
    91  	// expect a call to create the K8S secret
    92  	cli.EXPECT().
    93  		Create(gomock.Any(), gomock.Any(), gomock.Any()).
    94  		DoAndReturn(func(ctx context.Context, s *v1.Secret, opts ...client.CreateOption) error {
    95  			assertSecretValid(assert, s, secretData)
    96  			return nil
    97  		})
    98  
    99  	// expect a call to update the resource with a finalizer
   100  	cli.EXPECT().
   101  		Update(gomock.Any(), gomock.Any(), gomock.Any()).
   102  		DoAndReturn(func(ctx context.Context, secret *clustersv1alpha1.MultiClusterSecret, opts ...client.UpdateOption) error {
   103  			assert.True(len(secret.ObjectMeta.Finalizers) == 1, "Wrong number of finalizers")
   104  			assert.Equal(finalizerName, secret.ObjectMeta.Finalizers[0], "wrong finalizer")
   105  			return nil
   106  		})
   107  
   108  	// expect a call to update the status of the multicluster secret
   109  	doExpectStatusUpdateSucceeded(cli, mockStatusWriter, assert)
   110  
   111  	// create a request and reconcile it
   112  	request := clusterstest.NewRequest(namespace, crName)
   113  	reconciler := newSecretReconciler(cli)
   114  	result, err := reconciler.Reconcile(context.TODO(), request)
   115  
   116  	mocker.Finish()
   117  	assert.NoError(err)
   118  	assert.Equal(false, result.Requeue)
   119  }
   120  
   121  // TestReconcileUpdateSecret tests the path of reconciling a MultiClusterSecret when the underlying
   122  // secret already exists i.e. update
   123  // expect to update a K8S secret
   124  // GIVEN a MultiClusterSecret resource is created
   125  // WHEN the controller Reconcile function is called
   126  // THEN expect a Secret to be updated
   127  func TestReconcileUpdateSecret(t *testing.T) {
   128  	assert := asserts.New(t)
   129  
   130  	mocker := gomock.NewController(t)
   131  	cli := mocks.NewMockClient(mocker)
   132  	mockStatusWriter := mocks.NewMockStatusWriter(mocker)
   133  
   134  	newSecretData := map[string][]byte{"username": []byte("aaaaa")}
   135  	existingSecretData := map[string][]byte{"username": []byte("existing")}
   136  
   137  	mcSecretSample := getSampleMCSecret(namespace, crName, newSecretData)
   138  
   139  	// expect a call to fetch the MultiClusterSecret
   140  	doExpectGetMultiClusterSecret(cli, mcSecretSample, true)
   141  
   142  	// expect a call to fetch the managed cluster registration secret
   143  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   144  
   145  	// expect a call to fetch underlying secret, and return an existing secret
   146  	doExpectGetSecretExists(cli, mcSecretSample.ObjectMeta, existingSecretData)
   147  
   148  	// expect a call to update the K8S secret with the new secret data
   149  	cli.EXPECT().
   150  		Update(gomock.Any(), gomock.Any(), gomock.Any()).
   151  		DoAndReturn(func(ctx context.Context, s *v1.Secret, opts ...client.UpdateOption) error {
   152  			assertSecretValid(assert, s, newSecretData)
   153  			return nil
   154  		})
   155  
   156  	// expect a call to update the status of the multicluster secret
   157  	doExpectStatusUpdateSucceeded(cli, mockStatusWriter, assert)
   158  
   159  	// create a request and reconcile it
   160  	request := clusterstest.NewRequest(namespace, crName)
   161  	reconciler := newSecretReconciler(cli)
   162  	result, err := reconciler.Reconcile(context.TODO(), request)
   163  
   164  	mocker.Finish()
   165  	assert.NoError(err)
   166  	assert.Equal(false, result.Requeue)
   167  }
   168  
   169  // TestReconcileCreateSecretFailed tests the path of reconciling a MultiClusterSecret
   170  // when the underlying secret does not exist and fails to be created due to some error condition
   171  // GIVEN a MultiClusterSecret resource is created
   172  // WHEN the controller Reconcile function is called and create underlying secret fails
   173  // THEN expect the status of the MultiClusterSecret to be updated with failure information
   174  func TestReconcileCreateSecretFailed(t *testing.T) {
   175  	assert := asserts.New(t)
   176  
   177  	mocker := gomock.NewController(t)
   178  	cli := mocks.NewMockClient(mocker)
   179  	mockStatusWriter := mocks.NewMockStatusWriter(mocker)
   180  
   181  	secretData := map[string][]byte{"username": []byte("aaaaa")}
   182  
   183  	mcSecretSample := getSampleMCSecret(namespace, crName, secretData)
   184  
   185  	// expect a call to fetch the MultiClusterSecret
   186  	doExpectGetMultiClusterSecret(cli, mcSecretSample, false)
   187  
   188  	// expect a call to fetch the managed cluster registration secret
   189  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   190  
   191  	// expect a call to fetch existing corev1.Secret and return not found error, to simulate create case
   192  	cli.EXPECT().
   193  		Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: crName}, gomock.Not(gomock.Nil()), gomock.Any()).
   194  		Return(errors.NewNotFound(schema.GroupResource{Group: "", Resource: "Secret"}, crName))
   195  
   196  	// expect a call to create the K8S secret and fail the call
   197  	cli.EXPECT().
   198  		Create(gomock.Any(), gomock.Any(), gomock.Any()).
   199  		DoAndReturn(func(ctx context.Context, s *v1.Secret, opts ...client.CreateOption) error {
   200  			return errors.NewBadRequest("will not create it")
   201  		})
   202  
   203  	// expect that the status of MultiClusterSecret is updated to failed because we
   204  	// failed the underlying secret's creation
   205  	doExpectStatusUpdateFailed(cli, mockStatusWriter, assert)
   206  
   207  	// create a request and reconcile it
   208  	request := clusterstest.NewRequest(namespace, crName)
   209  	reconciler := newSecretReconciler(cli)
   210  	result, err := reconciler.Reconcile(context.TODO(), request)
   211  
   212  	mocker.Finish()
   213  	assert.Nil(err)
   214  	assert.Equal(true, result.Requeue)
   215  }
   216  
   217  func TestReconcileUpdateSecretFailed(t *testing.T) {
   218  	assert := asserts.New(t)
   219  
   220  	mocker := gomock.NewController(t)
   221  	cli := mocks.NewMockClient(mocker)
   222  	mockStatusWriter := mocks.NewMockStatusWriter(mocker)
   223  
   224  	secretData := map[string][]byte{"username": []byte("aaaaa")}
   225  	existingSecretData := map[string][]byte{"username": []byte("existing secret data")}
   226  
   227  	mcSecretSample := getSampleMCSecret(namespace, crName, secretData)
   228  
   229  	// expect a call to fetch the MultiClusterSecret
   230  	doExpectGetMultiClusterSecret(cli, mcSecretSample, true)
   231  
   232  	// expect a call to fetch the managed cluster registration secret
   233  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   234  
   235  	// expect a call to fetch existing corev1.Secret (simulate update case)
   236  	doExpectGetSecretExists(cli, mcSecretSample.ObjectMeta, existingSecretData)
   237  
   238  	// expect a call to update the K8S secret and fail the call
   239  	cli.EXPECT().
   240  		Update(gomock.Any(), gomock.Any(), gomock.Any()).
   241  		DoAndReturn(func(ctx context.Context, s *v1.Secret, opts ...client.UpdateOption) error {
   242  			return errors.NewBadRequest("will not update it")
   243  		})
   244  
   245  	// expect that the status of MultiClusterSecret is updated to failed because we
   246  	// failed the underlying secret's creation
   247  	doExpectStatusUpdateFailed(cli, mockStatusWriter, assert)
   248  
   249  	// create a request and reconcile it
   250  	request := clusterstest.NewRequest(namespace, crName)
   251  	reconciler := newSecretReconciler(cli)
   252  	result, err := reconciler.Reconcile(context.TODO(), request)
   253  
   254  	mocker.Finish()
   255  	assert.Nil(err)
   256  	assert.Equal(true, result.Requeue)
   257  }
   258  
   259  // TestReconcilePlacementInDifferentCluster tests the path of reconciling a MultiClusterSecret which
   260  // is placed on a cluster other than the current cluster. We expect this MultiClusterSecret to
   261  // be ignored, and no K8S secret to be created
   262  // GIVEN a MultiClusterSecret resource is created with a placement in different cluster
   263  // WHEN the controller Reconcile function is called
   264  // THEN expect that no K8S Secret is created
   265  func TestReconcilePlacementInDifferentCluster(t *testing.T) {
   266  	assert := asserts.New(t)
   267  
   268  	mocker := gomock.NewController(t)
   269  	cli := mocks.NewMockClient(mocker)
   270  	statusWriter := mocks.NewMockStatusWriter(mocker)
   271  
   272  	secretData := map[string][]byte{"username": []byte("aaaaa")}
   273  
   274  	mcSecretSample := getSampleMCSecret(namespace, crName, secretData)
   275  
   276  	mcSecretSample.Spec.Placement.Clusters[0].Name = "not-my-cluster"
   277  
   278  	// expect a call to fetch the MultiClusterSecret
   279  	doExpectGetMultiClusterSecret(cli, mcSecretSample, true)
   280  
   281  	// expect a call to fetch the MCRegistration secret
   282  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   283  
   284  	// The effective state of the object will get updated even if it is note locally placed,
   285  	// since it would have changed
   286  	clusterstest.DoExpectUpdateState(t, cli, statusWriter, &mcSecretSample, clustersv1alpha1.Pending)
   287  
   288  	clusterstest.ExpectDeleteAssociatedResource(cli, &v1alpha2.Component{
   289  		ObjectMeta: metav1.ObjectMeta{
   290  			Name:      mcSecretSample.Name,
   291  			Namespace: mcSecretSample.Namespace,
   292  		},
   293  	}, types.NamespacedName{
   294  		Namespace: mcSecretSample.Namespace,
   295  		Name:      mcSecretSample.Name,
   296  	})
   297  
   298  	// expect a call to update the resource with no finalizers
   299  	cli.EXPECT().
   300  		Update(gomock.Any(), gomock.Any(), gomock.Any()).
   301  		DoAndReturn(func(ctx context.Context, mcSecret *clustersv1alpha1.MultiClusterSecret, opts ...client.UpdateOption) error {
   302  			assert.True(len(mcSecret.Finalizers) == 0, "Wrong number of finalizers")
   303  			return nil
   304  		})
   305  
   306  	// Expect no further action
   307  
   308  	// create a request and reconcile it
   309  	request := clusterstest.NewRequest(namespace, crName)
   310  	reconciler := newSecretReconciler(cli)
   311  	result, err := reconciler.Reconcile(context.TODO(), request)
   312  
   313  	mocker.Finish()
   314  	assert.NoError(err)
   315  	assert.Equal(false, result.Requeue)
   316  }
   317  
   318  // TestReconcileResourceNotFound tests the path of reconciling a
   319  // MultiClusterSecret resource which is non-existent when reconcile is called,
   320  // possibly because it has been deleted.
   321  // GIVEN a MultiClusterSecret resource has been deleted
   322  // WHEN the controller Reconcile function is called
   323  // THEN expect that no action is taken
   324  func TestReconcileResourceNotFound(t *testing.T) {
   325  	assert := asserts.New(t)
   326  
   327  	mocker := gomock.NewController(t)
   328  	cli := mocks.NewMockClient(mocker)
   329  
   330  	// expect a call to fetch the MultiClusterLoggingScope
   331  	// and return a not found error
   332  	cli.EXPECT().
   333  		Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: crName}, gomock.Not(gomock.Nil()), gomock.Any()).
   334  		Return(errors.NewNotFound(schema.GroupResource{Group: clustersv1alpha1.SchemeGroupVersion.Group, Resource: clustersv1alpha1.MultiClusterSecretResource}, crName))
   335  
   336  	// expect no further action to be taken
   337  
   338  	// create a request and reconcile it
   339  	request := clusterstest.NewRequest(namespace, crName)
   340  	reconciler := newSecretReconciler(cli)
   341  	result, err := reconciler.Reconcile(context.TODO(), request)
   342  
   343  	mocker.Finish()
   344  	assert.NoError(err)
   345  	assert.Equal(false, result.Requeue)
   346  }
   347  
   348  // doExpectGetSecretExists expects a call to get a corev1.Secret, and return an "existing" secret
   349  func doExpectGetSecretExists(cli *mocks.MockClient, metadata metav1.ObjectMeta, existingSecretData map[string][]byte) {
   350  	cli.EXPECT().
   351  		Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: crName}, gomock.Not(gomock.Nil()), gomock.Any()).
   352  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, secret *v1.Secret, opts ...client.GetOption) error {
   353  			secret.Data = existingSecretData
   354  			secret.ObjectMeta = metadata
   355  			return nil
   356  		})
   357  }
   358  
   359  // doExpectStatusUpdateFailed expects a call to update status of MultiClusterSecret to failure
   360  func doExpectStatusUpdateFailed(cli *mocks.MockClient, mockStatusWriter *mocks.MockStatusWriter, assert *asserts.Assertions) {
   361  	// expect a call to fetch the MCRegistration secret to get the cluster name for status update
   362  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   363  
   364  	// expect a call to update the status of the multicluster secret
   365  	cli.EXPECT().Status().Return(mockStatusWriter)
   366  
   367  	// the status update should be to failure status/conditions on the multicluster secret
   368  	mockStatusWriter.EXPECT().
   369  		Update(gomock.Any(), gomock.AssignableToTypeOf(&clustersv1alpha1.MultiClusterSecret{}), gomock.Any()).
   370  		DoAndReturn(func(ctx context.Context, mcSecret *clustersv1alpha1.MultiClusterSecret, opts ...client.UpdateOption) error {
   371  			clusterstest.AssertMultiClusterResourceStatus(assert, mcSecret.Status,
   372  				clustersv1alpha1.Failed, clustersv1alpha1.DeployFailed, v1.ConditionTrue)
   373  			return nil
   374  		})
   375  }
   376  
   377  // doExpectStatusUpdateSucceeded expects a call to update status of MultiClusterSecret to success
   378  func doExpectStatusUpdateSucceeded(cli *mocks.MockClient, mockStatusWriter *mocks.MockStatusWriter, assert *asserts.Assertions) {
   379  	// expect a call to fetch the MCRegistration secret to get the cluster name for status update
   380  	clusterstest.DoExpectGetMCRegistrationSecret(cli)
   381  
   382  	// expect a call to update the status of the multicluster secret
   383  	cli.EXPECT().Status().Return(mockStatusWriter)
   384  
   385  	// the status update should be to success status/conditions on the multicluster secret
   386  	mockStatusWriter.EXPECT().
   387  		Update(gomock.Any(), gomock.AssignableToTypeOf(&clustersv1alpha1.MultiClusterSecret{}), gomock.Any()).
   388  		DoAndReturn(func(ctx context.Context, mcSecret *clustersv1alpha1.MultiClusterSecret, opts ...client.UpdateOption) error {
   389  			clusterstest.AssertMultiClusterResourceStatus(assert, mcSecret.Status,
   390  				clustersv1alpha1.Succeeded, clustersv1alpha1.DeployComplete, v1.ConditionTrue)
   391  			return nil
   392  		})
   393  }
   394  
   395  // doExpectGetMultiClusterSecret adds an expectation to the given MockClient to expect a Get
   396  // call for a MultiClusterSecret, and populate the multi cluster secret with given data
   397  func doExpectGetMultiClusterSecret(cli *mocks.MockClient, mcSecretSample clustersv1alpha1.MultiClusterSecret, addFinalizer bool) {
   398  	cli.EXPECT().
   399  		Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: crName}, gomock.AssignableToTypeOf(&mcSecretSample), gomock.Any()).
   400  		DoAndReturn(func(ctx context.Context, name types.NamespacedName, mcSecret *clustersv1alpha1.MultiClusterSecret, opts ...client.GetOption) error {
   401  			mcSecret.ObjectMeta = mcSecretSample.ObjectMeta
   402  			mcSecret.TypeMeta = mcSecretSample.TypeMeta
   403  			mcSecret.Spec = mcSecretSample.Spec
   404  			if addFinalizer {
   405  				mcSecret.Finalizers = append(mcSecret.Finalizers, finalizerName)
   406  			}
   407  			return nil
   408  		})
   409  }
   410  
   411  // assertSecretValid asserts that the metadata and content of the created/updated K8S secret
   412  // are valid
   413  func assertSecretValid(assert *asserts.Assertions, s *v1.Secret, secretData map[string][]byte) {
   414  	assert.Equal(v1.SecretTypeOpaque, s.Type)
   415  	assert.Equal(namespace, s.ObjectMeta.Namespace)
   416  	assert.Equal(crName, s.ObjectMeta.Name)
   417  	assert.Equal(secretData, s.Data)
   418  	// assert that the secret is labeled verrazzano-managed=true since it was created by Verrazzano
   419  	assert.NotNil(s.Labels)
   420  	assert.Equal(constants.LabelVerrazzanoManagedDefault, s.Labels[vzconst.VerrazzanoManagedLabelKey])
   421  }
   422  
   423  // getSampleMCSecret creates and returns a sample MultiClusterSecret used in tests
   424  func getSampleMCSecret(ns string, name string, secretData map[string][]byte) clustersv1alpha1.MultiClusterSecret {
   425  	var mcSecret clustersv1alpha1.MultiClusterSecret
   426  	mcSecret.Spec.Template = clustersv1alpha1.SecretTemplate{Type: v1.SecretTypeOpaque, Data: secretData}
   427  	mcSecret.ObjectMeta.Namespace = namespace
   428  	mcSecret.ObjectMeta.Name = crName
   429  	mcSecret.APIVersion = clustersv1alpha1.SchemeGroupVersion.String()
   430  	mcSecret.Kind = "MultiClusterSecret"
   431  	mcSecret.Spec.Placement.Clusters = []clustersv1alpha1.Cluster{{Name: clusterstest.UnitTestClusterName}}
   432  	return mcSecret
   433  }
   434  
   435  // newSecretReconciler creates a new reconciler for testing
   436  // c - The K8s client to inject into the reconciler
   437  func newSecretReconciler(c client.Client) Reconciler {
   438  	return Reconciler{
   439  		Client: c,
   440  		Log:    zap.S().With("test"),
   441  		Scheme: clusters.NewScheme(),
   442  	}
   443  }
   444  
   445  // TestReconcileKubeSystem tests to make sure we do not reconcile
   446  // Any resource that belong to the kube-system namespace
   447  func TestReconcileKubeSystem(t *testing.T) {
   448  	assert := asserts.New(t)
   449  
   450  	var mocker = gomock.NewController(t)
   451  	var cli = mocks.NewMockClient(mocker)
   452  
   453  	// create a request and reconcile it
   454  	request := clusterstest.NewRequest(vzconst.KubeSystem, "unit-test-verrazzano-helidon-workload")
   455  	reconciler := newSecretReconciler(cli)
   456  	result, err := reconciler.Reconcile(context.TODO(), request)
   457  
   458  	mocker.Finish()
   459  	assert.Nil(err)
   460  	assert.True(result.IsZero())
   461  }