github.com/verrazzano/verrazzano@v1.7.1/cluster-operator/controllers/vmc/update_status_test.go (about) 1 // Copyright (c) 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 vmc 5 6 import ( 7 "context" 8 "testing" 9 "time" 10 11 "github.com/golang/mock/gomock" 12 "github.com/stretchr/testify/assert" 13 "github.com/verrazzano/verrazzano/cluster-operator/apis/clusters/v1alpha1" 14 "github.com/verrazzano/verrazzano/cluster-operator/internal/capi" 15 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 16 "github.com/verrazzano/verrazzano/pkg/rancherutil" 17 vzv1beta1 "github.com/verrazzano/verrazzano/platform-operator/apis/verrazzano/v1beta1" 18 "github.com/verrazzano/verrazzano/platform-operator/constants" 19 "github.com/verrazzano/verrazzano/platform-operator/mocks" 20 corev1 "k8s.io/api/core/v1" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 23 "k8s.io/apimachinery/pkg/runtime" 24 "k8s.io/apimachinery/pkg/types" 25 "sigs.k8s.io/cluster-api/api/v1beta1" 26 "sigs.k8s.io/controller-runtime/pkg/client" 27 "sigs.k8s.io/controller-runtime/pkg/client/fake" 28 ) 29 30 const ( 31 testCAPIClusterName = "test-capi-cluster" 32 testClusterClassName = "test-cluster-class" 33 testCAPINamespace = "c-12345" 34 testVMCName = "test-vmc" 35 testVMCNamespace = "verrazzano-mc" 36 fakeControlPlaneProviderAPIVersion = "controlPlaneAPIversion" 37 fakeControlPlaneProviderKind = "controlPlaneKind" 38 ) 39 40 // TestUpdateStatus tests the updateStatus function 41 func TestUpdateStatus(t *testing.T) { 42 // clear any cached user auth tokens when the test completes 43 defer rancherutil.DeleteStoredTokens() 44 45 asserts := assert.New(t) 46 mocker := gomock.NewController(t) 47 mock := mocks.NewMockClient(mocker) 48 mockStatus := mocks.NewMockStatusWriter(mocker) 49 asserts.NotNil(mockStatus) 50 51 // Expect the requests for the existing VMC resource 52 mock.EXPECT().Get(gomock.Any(), types.NamespacedName{Namespace: constants.VerrazzanoMultiClusterNamespace, Name: testManagedCluster}, gomock.AssignableToTypeOf(&v1alpha1.VerrazzanoManagedCluster{}), gomock.Any()). 53 DoAndReturn(func(ctx context.Context, nsn types.NamespacedName, vmc *v1alpha1.VerrazzanoManagedCluster, opts ...client.GetOption) error { 54 return nil 55 }).AnyTimes() 56 57 // GIVEN a VMC with a status state unset and the last agent connect time set 58 // WHEN the updateStatus function is called 59 // THEN the status state is updated to pending 60 mock.EXPECT().Status().Return(mockStatus) 61 mockStatus.EXPECT(). 62 Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.VerrazzanoManagedCluster{}), gomock.Any()). 63 DoAndReturn(func(ctx context.Context, vmc *v1alpha1.VerrazzanoManagedCluster, opts ...client.UpdateOption) error { 64 asserts.Equal(v1alpha1.StatePending, vmc.Status.State) 65 return nil 66 }) 67 68 vmc := v1alpha1.VerrazzanoManagedCluster{ 69 ObjectMeta: metav1.ObjectMeta{ 70 Name: testManagedCluster, 71 Namespace: constants.VerrazzanoMultiClusterNamespace, 72 }, 73 Status: v1alpha1.VerrazzanoManagedClusterStatus{ 74 LastAgentConnectTime: &metav1.Time{ 75 Time: time.Now(), 76 }, 77 }, 78 } 79 reconciler := newVMCReconciler(mock) 80 reconciler.log = vzlog.DefaultLogger() 81 82 err := reconciler.updateStatus(context.TODO(), &vmc) 83 84 // Validate the results 85 mocker.Finish() 86 asserts.NoError(err) 87 88 // GIVEN a VMC with a status state of pending and the last agent connect time set 89 // WHEN the updateStatus function is called 90 // THEN the status state is updated to active 91 mock.EXPECT().Status().Return(mockStatus) 92 mockStatus.EXPECT(). 93 Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.VerrazzanoManagedCluster{}), gomock.Any()). 94 DoAndReturn(func(ctx context.Context, vmc *v1alpha1.VerrazzanoManagedCluster, opts ...client.UpdateOption) error { 95 asserts.Equal(v1alpha1.StateActive, vmc.Status.State) 96 return nil 97 }) 98 99 err = reconciler.updateStatus(context.TODO(), &vmc) 100 101 // Validate the results 102 mocker.Finish() 103 asserts.NoError(err) 104 105 // GIVEN a VMC with a last agent connect time set in the past 106 // WHEN the updateStatus function is called 107 // THEN the status state is updated to inactive 108 past := metav1.Unix(0, 0) 109 vmc.Status.LastAgentConnectTime = &past 110 111 // Expect the Rancher registration status to be set appropriately 112 mock.EXPECT().Status().Return(mockStatus) 113 mockStatus.EXPECT(). 114 Update(gomock.Any(), gomock.AssignableToTypeOf(&v1alpha1.VerrazzanoManagedCluster{}), gomock.Any()). 115 DoAndReturn(func(ctx context.Context, vmc *v1alpha1.VerrazzanoManagedCluster, opts ...client.UpdateOption) error { 116 asserts.Equal(v1alpha1.StateInactive, vmc.Status.State) 117 return nil 118 }) 119 120 err = reconciler.updateStatus(context.TODO(), &vmc) 121 122 // Validate the results 123 mocker.Finish() 124 asserts.NoError(err) 125 } 126 127 // TestUpdateStatusImported tests that updateStatus correctly sets the VMC's status.imported field 128 func TestUpdateStatusImported(t *testing.T) { 129 a := assert.New(t) 130 scheme := runtime.NewScheme() 131 _ = v1alpha1.AddToScheme(scheme) 132 _ = v1beta1.AddToScheme(scheme) 133 134 defaultCAPIClientFunc := getCAPIClientFunc 135 defer func() { getCAPIClientFunc = defaultCAPIClientFunc }() 136 137 tests := []struct { 138 testName string 139 vmc *v1alpha1.VerrazzanoManagedCluster 140 expectedImported bool 141 }{ 142 { 143 "imported cluster", 144 newVMC(testVMCName, testVMCNamespace), 145 true, 146 }, 147 { 148 "ClusterAPI cluster", 149 newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace), 150 false, 151 }, 152 } 153 154 for _, tt := range tests { 155 t.Run(tt.testName, func(t *testing.T) { 156 // GIVEN a VMC with either a nil or non-nil ClusterRef 157 // WHEN updateStatus is called 158 // THEN expect the VMC's status imported field to be set 159 getCAPIClientFunc = fakeCAPIClient 160 ctx := context.TODO() 161 fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.vmc).Build() 162 r := &VerrazzanoManagedClusterReconciler{ 163 Client: fakeClient, 164 log: vzlog.DefaultLogger(), 165 } 166 167 err := r.updateStatus(ctx, tt.vmc) 168 a.NoError(err) 169 170 retrievedVMC := &v1alpha1.VerrazzanoManagedCluster{} 171 err = r.Get(ctx, types.NamespacedName{Name: tt.vmc.Name, Namespace: tt.vmc.Namespace}, retrievedVMC) 172 a.NoError(err) 173 a.Equal(tt.expectedImported, *retrievedVMC.Status.Imported) 174 }) 175 } 176 } 177 178 // TestUpdateProvider tests that updateProvider correctly sets the VMC's status.provider field 179 func TestUpdateProvider(t *testing.T) { 180 a := assert.New(t) 181 scheme := runtime.NewScheme() 182 _ = v1alpha1.AddToScheme(scheme) 183 _ = v1beta1.AddToScheme(scheme) 184 185 tests := []struct { 186 testName string 187 vmc *v1alpha1.VerrazzanoManagedCluster 188 capiCluster *v1beta1.Cluster 189 clusterClass *v1beta1.ClusterClass 190 controlPlaneProvider string 191 infraProvider string 192 expectedVMCProvider string 193 err error 194 }{ 195 { 196 "imported cluster", 197 newVMC(testVMCName, testVMCNamespace), 198 nil, 199 nil, 200 "", 201 "", 202 importedProviderDisplayName, 203 nil, 204 }, 205 { 206 "CAPI Cluster without ClusterClass and a generic provider", 207 newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace), 208 newCAPICluster(testCAPIClusterName, testCAPINamespace), 209 nil, 210 "SomeControlPlaneProvider", 211 "SomeInfraProvider", 212 "SomeControlPlaneProvider on SomeInfraProvider Infrastructure", 213 nil, 214 }, 215 { 216 "CAPI Cluster without ClusterClass and Oracle OKE Provider", 217 newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace), 218 newCAPICluster(testCAPIClusterName, testCAPINamespace), 219 nil, 220 capi.OKEControlPlaneProvider, 221 capi.OKEInfrastructureProvider, 222 okeProviderDisplayName, 223 nil, 224 }, 225 { 226 "CAPI Cluster with ClusterClass and Oracle OCNE Provider", 227 newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace), 228 newCAPIClusterWithClassReference(testCAPIClusterName, testClusterClassName, testCAPINamespace), 229 newCAPIClusterClass(testClusterClassName, testCAPINamespace), 230 capi.OCNEControlPlaneProvider, 231 capi.OCNEInfrastructureProvider, 232 ocneProviderDisplayName, 233 nil, 234 }, 235 } 236 237 for _, tt := range tests { 238 t.Run(tt.testName, func(t *testing.T) { 239 objects := []client.Object{tt.vmc} 240 if tt.capiCluster != nil { 241 if tt.clusterClass == nil { 242 tt.capiCluster.Spec.InfrastructureRef.Kind = tt.infraProvider 243 tt.capiCluster.Spec.ControlPlaneRef.Kind = tt.controlPlaneProvider 244 } 245 objects = append(objects, tt.capiCluster) 246 } 247 if tt.clusterClass != nil { 248 tt.clusterClass.Spec.Infrastructure.Ref.Kind = tt.infraProvider 249 tt.clusterClass.Spec.ControlPlane.Ref.Kind = tt.controlPlaneProvider 250 objects = append(objects, tt.clusterClass) 251 } 252 fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() 253 r := &VerrazzanoManagedClusterReconciler{ 254 Client: fakeClient, 255 log: vzlog.DefaultLogger(), 256 } 257 258 // WHEN updateProvider is called 259 // THEN expect the VMC's status provider to be tt.expectedVMCProvider 260 err := r.updateProvider(tt.vmc) 261 262 a.Equal(tt.err, err) 263 a.Equal(tt.expectedVMCProvider, tt.vmc.Status.Provider) 264 }) 265 } 266 } 267 268 // TestUpdateStateCAPI tests that updateState correctly updates the status.state of the VMC when 269 // the VMC has a reference to a CAPI cluster 270 func TestUpdateStateCAPI(t *testing.T) { 271 a := assert.New(t) 272 scheme := runtime.NewScheme() 273 _ = v1alpha1.AddToScheme(scheme) 274 _ = v1beta1.AddToScheme(scheme) 275 vmc := newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace) 276 277 tests := []struct { 278 testName string 279 capiCluster *v1beta1.Cluster 280 capiPhase string 281 expectedVMCState string 282 err error 283 }{ 284 { 285 "valid CAPI phase", 286 newCAPICluster(testCAPIClusterName, testCAPINamespace), 287 string(v1alpha1.StateProvisioned), 288 string(v1alpha1.StateProvisioned), 289 nil, 290 }, 291 { 292 "empty CAPI phase", 293 newCAPICluster(testCAPIClusterName, testCAPINamespace), 294 "", 295 string(v1alpha1.StateUnknown), 296 nil, 297 }, 298 { 299 "nonexistent CAPI cluster", 300 nil, 301 "", 302 "", 303 nil, 304 }, 305 } 306 307 for _, tt := range tests { 308 t.Run(tt.testName, func(t *testing.T) { 309 // GIVEN a CAPI cluster with a phase of tt.capiPhase 310 // and a VMC with a clusterRef to that CAPI cluster and an unset status state 311 // WHEN updateState is called 312 // THEN expect the VMC's status state to be tt.expectedVMCState 313 vmc.Status.State = "" 314 objects := []client.Object{vmc} 315 if tt.capiCluster != nil { 316 tt.capiCluster.Status.Phase = tt.capiPhase 317 objects = append(objects, tt.capiCluster) 318 } 319 fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() 320 r := &VerrazzanoManagedClusterReconciler{ 321 Client: fakeClient, 322 log: vzlog.DefaultLogger(), 323 } 324 325 err := r.updateState(vmc) 326 327 a.Equal(tt.err, err) 328 a.Equal(tt.expectedVMCState, string(vmc.Status.State)) 329 }) 330 } 331 } 332 333 // TestShouldUpdateK8sVersion tests that shouldUpdateK8sVersion correctly determines when the VMC controller should 334 // update the VMC's Kubernetes version. 335 func TestShouldUpdateK8sVersion(t *testing.T) { 336 a := assert.New(t) 337 scheme := runtime.NewScheme() 338 _ = v1alpha1.AddToScheme(scheme) 339 _ = v1beta1.AddToScheme(scheme) 340 _ = vzv1beta1.AddToScheme(scheme) 341 342 defaultCAPIClientFunc := getCAPIClientFunc 343 defer func() { getCAPIClientFunc = defaultCAPIClientFunc }() 344 345 tests := []struct { 346 testName string 347 setClusterRef bool 348 vzOnCAPI bool 349 shouldUpdate bool 350 err error 351 }{ 352 { 353 "non-ClusterAPI cluster", 354 false, 355 false, 356 false, 357 nil, 358 }, 359 { 360 "ClusterAPI cluster without Verrazzano", 361 true, 362 false, 363 true, 364 nil, 365 }, 366 { 367 "ClusterAPI cluster with Verrazzano", 368 true, 369 true, 370 false, 371 nil, 372 }, 373 } 374 375 for _, tt := range tests { 376 t.Run(tt.testName, func(t *testing.T) { 377 var vmc *v1alpha1.VerrazzanoManagedCluster 378 if tt.setClusterRef { 379 if tt.vzOnCAPI { 380 getCAPIClientFunc = fakeCAPIClientWithVZ 381 } else { 382 getCAPIClientFunc = fakeCAPIClient 383 } 384 vmc = newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace) 385 } else { 386 vmc = newVMC(testVMCName, testVMCNamespace) 387 } 388 fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vmc).Build() 389 r := &VerrazzanoManagedClusterReconciler{ 390 Client: fakeClient, 391 Scheme: scheme, 392 log: vzlog.DefaultLogger(), 393 } 394 395 shouldUpdate, err := r.shouldUpdateK8sVersion(vmc) 396 a.Equal(tt.err, err) 397 a.Equal(tt.shouldUpdate, shouldUpdate) 398 }) 399 } 400 } 401 402 // TestUpdateK8sVersionUsingCAPI tests that updateK8sVersionUsingCAPI correctly updates the Kubernetes version 403 // on the VMC 404 func TestUpdateK8sVersionUsingCAPI(t *testing.T) { 405 const cpProviderName = "SomeControlPlaneProvider" 406 const expectedK8sVersion = "v9.99.9" 407 408 a := assert.New(t) 409 scheme := runtime.NewScheme() 410 _ = v1alpha1.AddToScheme(scheme) 411 _ = v1beta1.AddToScheme(scheme) 412 413 // Create a VMC that references a CAPI cluster 414 vmc := newVMCWithClusterRef(testVMCName, testVMCNamespace, testCAPIClusterName, testCAPINamespace) 415 // The CAPI cluster references some control plane provider 416 cluster := newCAPICluster(testCAPIClusterName, testCAPINamespace) 417 cluster.Spec.ControlPlaneRef = &corev1.ObjectReference{ 418 Name: cpProviderName, 419 Namespace: testCAPINamespace, 420 APIVersion: fakeControlPlaneProviderAPIVersion, 421 Kind: fakeControlPlaneProviderKind, 422 } 423 // Create the control plane provider CR on the fake client 424 cpProvider := newFakeControlPlaneProvider(testCAPINamespace, cpProviderName, expectedK8sVersion) 425 fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(vmc, cluster, cpProvider).Build() 426 r := &VerrazzanoManagedClusterReconciler{ 427 Client: fakeClient, 428 Scheme: scheme, 429 log: vzlog.DefaultLogger(), 430 } 431 432 err := r.updateK8sVersionUsingCAPI(vmc) 433 a.Nil(err) 434 a.Equal(expectedK8sVersion, vmc.Status.Kubernetes.Version) 435 } 436 437 // fakeCAPIClient returns a fake client for a CAPI workload cluster 438 func fakeCAPIClient(ctx context.Context, cli client.Client, cluster types.NamespacedName, scheme *runtime.Scheme) (client.Client, error) { 439 return fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build(), nil 440 } 441 442 // fakeCAPIClient returns a fake client for a CAPI workload cluster 443 func fakeCAPIClientWithVZ(ctx context.Context, cli client.Client, cluster types.NamespacedName, scheme *runtime.Scheme) (client.Client, error) { 444 vz := vzv1beta1.Verrazzano{} 445 return fake.NewClientBuilder().WithScheme(scheme).WithObjects(&vz).Build(), nil 446 } 447 448 // newVMCWithClusterRef returns a VMC struct pointer, with the status.clusterRef field set to point to a CAPI Cluster 449 func newVMCWithClusterRef(name, namespace, clusterName, clusterNamespace string) *v1alpha1.VerrazzanoManagedCluster { 450 vmc := &v1alpha1.VerrazzanoManagedCluster{ 451 ObjectMeta: metav1.ObjectMeta{ 452 Name: name, 453 Namespace: namespace, 454 }, 455 Status: v1alpha1.VerrazzanoManagedClusterStatus{ 456 ClusterRef: &v1alpha1.ClusterReference{ 457 Name: clusterName, 458 Namespace: clusterNamespace, 459 APIVersion: "cluster.x-k8s.io/v1beta1", 460 Kind: "Cluster", 461 }, 462 }, 463 } 464 return vmc 465 } 466 467 // newVMC returns a VMC struct pointer 468 func newVMC(name, namespace string) *v1alpha1.VerrazzanoManagedCluster { 469 vmc := &v1alpha1.VerrazzanoManagedCluster{ 470 ObjectMeta: metav1.ObjectMeta{ 471 Name: name, 472 Namespace: namespace, 473 }, 474 } 475 return vmc 476 } 477 478 // newCAPICluster returns a CAPI Cluster 479 func newCAPICluster(name, namespace string) *v1beta1.Cluster { 480 cluster := v1beta1.Cluster{ 481 TypeMeta: metav1.TypeMeta{ 482 Kind: "Cluster", 483 APIVersion: "cluster.x-k8s.io/v1beta1", 484 }, 485 ObjectMeta: metav1.ObjectMeta{ 486 Name: name, 487 Namespace: namespace, 488 }, 489 Spec: v1beta1.ClusterSpec{ 490 InfrastructureRef: &corev1.ObjectReference{}, 491 ControlPlaneRef: &corev1.ObjectReference{}, 492 }, 493 } 494 return &cluster 495 } 496 497 // newCAPIClusterWithClassReference returns a CAPI Cluster which references a ClusterClass 498 func newCAPIClusterWithClassReference(name, className, namespace string) *v1beta1.Cluster { 499 cluster := v1beta1.Cluster{ 500 TypeMeta: metav1.TypeMeta{ 501 Kind: "Cluster", 502 APIVersion: "cluster.x-k8s.io/v1beta1", 503 }, 504 ObjectMeta: metav1.ObjectMeta{ 505 Name: name, 506 Namespace: namespace, 507 }, 508 Spec: v1beta1.ClusterSpec{ 509 Topology: &v1beta1.Topology{ 510 Class: className, 511 }, 512 }, 513 } 514 return &cluster 515 } 516 517 // newCAPIClusterClass returns a CAPI ClusterClass 518 func newCAPIClusterClass(name, namespace string) *v1beta1.ClusterClass { 519 clusterClass := v1beta1.ClusterClass{ 520 TypeMeta: metav1.TypeMeta{ 521 Kind: "ClusterClass", 522 APIVersion: "cluster.x-k8s.io/v1beta1", 523 }, 524 ObjectMeta: metav1.ObjectMeta{ 525 Name: name, 526 Namespace: namespace, 527 }, 528 Spec: v1beta1.ClusterClassSpec{ 529 Infrastructure: v1beta1.LocalObjectTemplate{ 530 Ref: &corev1.ObjectReference{}, 531 }, 532 ControlPlane: v1beta1.ControlPlaneClass{ 533 LocalObjectTemplate: v1beta1.LocalObjectTemplate{ 534 Ref: &corev1.ObjectReference{}, 535 }, 536 }, 537 }, 538 } 539 return &clusterClass 540 } 541 542 // newFakeControlPlaneProvider returns a pointer to an unstructured object, representing a control 543 // plane provider CR 544 func newFakeControlPlaneProvider(namespace, name, k8sVersion string) *unstructured.Unstructured { 545 return &unstructured.Unstructured{ 546 Object: map[string]interface{}{ 547 "apiVersion": fakeControlPlaneProviderAPIVersion, 548 "kind": fakeControlPlaneProviderKind, 549 "metadata": map[string]interface{}{ 550 "namespace": namespace, 551 "name": name, 552 }, 553 "status": map[string]interface{}{ 554 "version": k8sVersion, 555 }, 556 }, 557 } 558 }