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