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