github.com/verrazzano/verrazzano@v1.7.0/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 }