sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/machineset/machineset_controller_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package machineset 18 19 import ( 20 "testing" 21 "time" 22 23 . "github.com/onsi/gomega" 24 corev1 "k8s.io/api/core/v1" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 "k8s.io/client-go/tools/record" 29 utilfeature "k8s.io/component-base/featuregate/testing" 30 "k8s.io/utils/ptr" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/client/fake" 33 "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 "sigs.k8s.io/cluster-api/controllers/external" 37 "sigs.k8s.io/cluster-api/feature" 38 "sigs.k8s.io/cluster-api/internal/contract" 39 "sigs.k8s.io/cluster-api/internal/test/builder" 40 "sigs.k8s.io/cluster-api/internal/util/ssa" 41 "sigs.k8s.io/cluster-api/util" 42 "sigs.k8s.io/cluster-api/util/conditions" 43 "sigs.k8s.io/cluster-api/util/patch" 44 ) 45 46 var _ reconcile.Reconciler = &Reconciler{} 47 48 func TestMachineSetReconciler(t *testing.T) { 49 setup := func(t *testing.T, g *WithT) (*corev1.Namespace, *clusterv1.Cluster) { 50 t.Helper() 51 52 t.Log("Creating the namespace") 53 ns, err := env.CreateNamespace(ctx, "test-machine-set-reconciler") 54 g.Expect(err).ToNot(HaveOccurred()) 55 56 t.Log("Creating the Cluster") 57 cluster := &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: ns.Name, Name: testClusterName}} 58 g.Expect(env.Create(ctx, cluster)).To(Succeed()) 59 60 t.Log("Creating the Cluster Kubeconfig Secret") 61 g.Expect(env.CreateKubeconfigSecret(ctx, cluster)).To(Succeed()) 62 63 return ns, cluster 64 } 65 66 teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace, cluster *clusterv1.Cluster) { 67 t.Helper() 68 69 t.Log("Deleting the Cluster") 70 g.Expect(env.Delete(ctx, cluster)).To(Succeed()) 71 t.Log("Deleting the namespace") 72 g.Expect(env.Delete(ctx, ns)).To(Succeed()) 73 } 74 75 t.Run("Should reconcile a MachineSet", func(t *testing.T) { 76 g := NewWithT(t) 77 namespace, testCluster := setup(t, g) 78 defer teardown(t, g, namespace, testCluster) 79 80 duration10m := &metav1.Duration{Duration: 10 * time.Minute} 81 duration5m := &metav1.Duration{Duration: 5 * time.Minute} 82 replicas := int32(2) 83 version := "v1.14.2" 84 instance := &clusterv1.MachineSet{ 85 ObjectMeta: metav1.ObjectMeta{ 86 GenerateName: "ms-", 87 Namespace: namespace.Name, 88 Labels: map[string]string{ 89 "label-1": "true", 90 }, 91 }, 92 Spec: clusterv1.MachineSetSpec{ 93 ClusterName: testCluster.Name, 94 Replicas: &replicas, 95 Selector: metav1.LabelSelector{ 96 MatchLabels: map[string]string{ 97 "label-1": "true", 98 }, 99 }, 100 Template: clusterv1.MachineTemplateSpec{ 101 ObjectMeta: clusterv1.ObjectMeta{ 102 Labels: map[string]string{ 103 "label-1": "true", 104 }, 105 Annotations: map[string]string{ 106 "annotation-1": "true", 107 "precedence": "MachineSet", 108 }, 109 }, 110 Spec: clusterv1.MachineSpec{ 111 ClusterName: testCluster.Name, 112 Version: &version, 113 Bootstrap: clusterv1.Bootstrap{ 114 ConfigRef: &corev1.ObjectReference{ 115 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 116 Kind: "GenericBootstrapConfigTemplate", 117 Name: "ms-template", 118 }, 119 }, 120 InfrastructureRef: corev1.ObjectReference{ 121 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 122 Kind: "GenericInfrastructureMachineTemplate", 123 Name: "ms-template", 124 }, 125 NodeDrainTimeout: duration10m, 126 NodeDeletionTimeout: duration10m, 127 NodeVolumeDetachTimeout: duration10m, 128 }, 129 }, 130 }, 131 } 132 133 // Create bootstrap template resource. 134 bootstrapResource := map[string]interface{}{ 135 "kind": "GenericBootstrapConfig", 136 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 137 "metadata": map[string]interface{}{ 138 "annotations": map[string]interface{}{ 139 "precedence": "GenericBootstrapConfig", 140 }, 141 }, 142 } 143 bootstrapTmpl := &unstructured.Unstructured{ 144 Object: map[string]interface{}{ 145 "spec": map[string]interface{}{ 146 "template": bootstrapResource, 147 }, 148 }, 149 } 150 bootstrapTmpl.SetKind("GenericBootstrapConfigTemplate") 151 bootstrapTmpl.SetAPIVersion("bootstrap.cluster.x-k8s.io/v1beta1") 152 bootstrapTmpl.SetName("ms-template") 153 bootstrapTmpl.SetNamespace(namespace.Name) 154 g.Expect(env.Create(ctx, bootstrapTmpl)).To(Succeed()) 155 156 // Create infrastructure template resource. 157 infraResource := map[string]interface{}{ 158 "kind": "GenericInfrastructureMachine", 159 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 160 "metadata": map[string]interface{}{ 161 "annotations": map[string]interface{}{ 162 "precedence": "GenericInfrastructureMachineTemplate", 163 }, 164 }, 165 "spec": map[string]interface{}{ 166 "size": "3xlarge", 167 }, 168 } 169 infraTmpl := &unstructured.Unstructured{ 170 Object: map[string]interface{}{ 171 "spec": map[string]interface{}{ 172 "template": infraResource, 173 }, 174 }, 175 } 176 infraTmpl.SetKind("GenericInfrastructureMachineTemplate") 177 infraTmpl.SetAPIVersion("infrastructure.cluster.x-k8s.io/v1beta1") 178 infraTmpl.SetName("ms-template") 179 infraTmpl.SetNamespace(namespace.Name) 180 g.Expect(env.Create(ctx, infraTmpl)).To(Succeed()) 181 182 // Create the MachineSet. 183 g.Expect(env.Create(ctx, instance)).To(Succeed()) 184 defer func() { 185 g.Expect(env.Delete(ctx, instance)).To(Succeed()) 186 }() 187 188 t.Log("Verifying the linked bootstrap template has a cluster owner reference") 189 g.Eventually(func() bool { 190 obj, err := external.Get(ctx, env, instance.Spec.Template.Spec.Bootstrap.ConfigRef, instance.Namespace) 191 if err != nil { 192 return false 193 } 194 195 return util.HasOwnerRef(obj.GetOwnerReferences(), metav1.OwnerReference{ 196 APIVersion: clusterv1.GroupVersion.String(), 197 Kind: "Cluster", 198 Name: testCluster.Name, 199 UID: testCluster.UID, 200 }) 201 }, timeout).Should(BeTrue()) 202 203 t.Log("Verifying the linked infrastructure template has a cluster owner reference") 204 g.Eventually(func() bool { 205 obj, err := external.Get(ctx, env, &instance.Spec.Template.Spec.InfrastructureRef, instance.Namespace) 206 if err != nil { 207 return false 208 } 209 210 return util.HasOwnerRef(obj.GetOwnerReferences(), metav1.OwnerReference{ 211 APIVersion: clusterv1.GroupVersion.String(), 212 Kind: "Cluster", 213 Name: testCluster.Name, 214 UID: testCluster.UID, 215 }) 216 }, timeout).Should(BeTrue()) 217 218 machines := &clusterv1.MachineList{} 219 220 // Verify that we have 2 replicas. 221 g.Eventually(func() int { 222 if err := env.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil { 223 return -1 224 } 225 return len(machines.Items) 226 }, timeout).Should(BeEquivalentTo(replicas)) 227 228 t.Log("Creating a InfrastructureMachine for each Machine") 229 infraMachines := &unstructured.UnstructuredList{} 230 infraMachines.SetAPIVersion("infrastructure.cluster.x-k8s.io/v1beta1") 231 infraMachines.SetKind("GenericInfrastructureMachine") 232 g.Eventually(func() int { 233 if err := env.List(ctx, infraMachines, client.InNamespace(namespace.Name)); err != nil { 234 return -1 235 } 236 return len(machines.Items) 237 }, timeout).Should(BeEquivalentTo(replicas)) 238 for _, im := range infraMachines.Items { 239 g.Expect(im.GetAnnotations()).To(HaveKeyWithValue("annotation-1", "true"), "have annotations of MachineTemplate applied") 240 g.Expect(im.GetAnnotations()).To(HaveKeyWithValue("precedence", "MachineSet"), "the annotations from the MachineSpec template to overwrite the infrastructure template ones") 241 g.Expect(im.GetLabels()).To(HaveKeyWithValue("label-1", "true"), "have labels of MachineTemplate applied") 242 } 243 g.Eventually(func() bool { 244 g.Expect(env.List(ctx, infraMachines, client.InNamespace(namespace.Name))).To(Succeed()) 245 // The Machine reconciler should remove the ownerReference to the MachineSet on the InfrastructureMachine. 246 hasMSOwnerRef := false 247 hasMachineOwnerRef := false 248 for _, im := range infraMachines.Items { 249 for _, o := range im.GetOwnerReferences() { 250 if o.Kind == machineSetKind.Kind { 251 hasMSOwnerRef = true 252 } 253 if o.Kind == "Machine" { 254 hasMachineOwnerRef = true 255 } 256 } 257 } 258 return !hasMSOwnerRef && hasMachineOwnerRef 259 }, timeout).Should(BeTrue(), "infraMachine should not have ownerRef to MachineSet") 260 261 t.Log("Creating a BootstrapConfig for each Machine") 262 bootstrapConfigs := &unstructured.UnstructuredList{} 263 bootstrapConfigs.SetAPIVersion("bootstrap.cluster.x-k8s.io/v1beta1") 264 bootstrapConfigs.SetKind("GenericBootstrapConfig") 265 g.Eventually(func() int { 266 if err := env.List(ctx, bootstrapConfigs, client.InNamespace(namespace.Name)); err != nil { 267 return -1 268 } 269 return len(machines.Items) 270 }, timeout).Should(BeEquivalentTo(replicas)) 271 for _, im := range bootstrapConfigs.Items { 272 g.Expect(im.GetAnnotations()).To(HaveKeyWithValue("annotation-1", "true"), "have annotations of MachineTemplate applied") 273 g.Expect(im.GetAnnotations()).To(HaveKeyWithValue("precedence", "MachineSet"), "the annotations from the MachineSpec template to overwrite the bootstrap config template ones") 274 g.Expect(im.GetLabels()).To(HaveKeyWithValue("label-1", "true"), "have labels of MachineTemplate applied") 275 } 276 g.Eventually(func() bool { 277 g.Expect(env.List(ctx, bootstrapConfigs, client.InNamespace(namespace.Name))).To(Succeed()) 278 // The Machine reconciler should remove the ownerReference to the MachineSet on the Bootstrap object. 279 hasMSOwnerRef := false 280 hasMachineOwnerRef := false 281 for _, im := range bootstrapConfigs.Items { 282 for _, o := range im.GetOwnerReferences() { 283 if o.Kind == machineSetKind.Kind { 284 hasMSOwnerRef = true 285 } 286 if o.Kind == "Machine" { 287 hasMachineOwnerRef = true 288 } 289 } 290 } 291 return !hasMSOwnerRef && hasMachineOwnerRef 292 }, timeout).Should(BeTrue(), "bootstrap should not have ownerRef to MachineSet") 293 294 // Set the infrastructure reference as ready. 295 for _, m := range machines.Items { 296 fakeBootstrapRefReady(*m.Spec.Bootstrap.ConfigRef, bootstrapResource, g) 297 fakeInfrastructureRefReady(m.Spec.InfrastructureRef, infraResource, g) 298 } 299 300 // Verify that in-place mutable fields propagate from MachineSet to Machines. 301 t.Log("Updating NodeDrainTimeout on MachineSet") 302 patchHelper, err := patch.NewHelper(instance, env) 303 g.Expect(err).ToNot(HaveOccurred()) 304 instance.Spec.Template.Spec.NodeDrainTimeout = duration5m 305 g.Expect(patchHelper.Patch(ctx, instance)).Should(Succeed()) 306 307 t.Log("Verifying new NodeDrainTimeout value is set on Machines") 308 g.Eventually(func() bool { 309 if err := env.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil { 310 return false 311 } 312 // All the machines should have the new NodeDrainTimeoutValue 313 for _, m := range machines.Items { 314 if m.Spec.NodeDrainTimeout == nil { 315 return false 316 } 317 if m.Spec.NodeDrainTimeout.Duration != duration5m.Duration { 318 return false 319 } 320 } 321 return true 322 }, timeout).Should(BeTrue(), "machine should have the updated NodeDrainTimeout value") 323 324 // Try to delete 1 machine and check the MachineSet scales back up. 325 machineToBeDeleted := machines.Items[0] 326 g.Expect(env.Delete(ctx, &machineToBeDeleted)).To(Succeed()) 327 328 // Verify that the Machine has been deleted. 329 g.Eventually(func() bool { 330 key := client.ObjectKey{Name: machineToBeDeleted.Name, Namespace: machineToBeDeleted.Namespace} 331 if err := env.Get(ctx, key, &machineToBeDeleted); apierrors.IsNotFound(err) || !machineToBeDeleted.DeletionTimestamp.IsZero() { 332 return true 333 } 334 return false 335 }, timeout).Should(BeTrue()) 336 337 // Verify that we have 2 replicas. 338 g.Eventually(func() (ready int) { 339 if err := env.List(ctx, machines, client.InNamespace(namespace.Name)); err != nil { 340 return -1 341 } 342 for _, m := range machines.Items { 343 if !m.DeletionTimestamp.IsZero() { 344 continue 345 } 346 ready++ 347 } 348 return 349 }, timeout*3).Should(BeEquivalentTo(replicas)) 350 351 // Verify that each machine has the desired kubelet version, 352 // create a fake node in Ready state, update NodeRef, and wait for a reconciliation request. 353 for i := 0; i < len(machines.Items); i++ { 354 m := machines.Items[i] 355 if !m.DeletionTimestamp.IsZero() { 356 // Skip deleted Machines 357 continue 358 } 359 360 g.Expect(m.Spec.Version).ToNot(BeNil()) 361 g.Expect(*m.Spec.Version).To(BeEquivalentTo("v1.14.2")) 362 fakeBootstrapRefReady(*m.Spec.Bootstrap.ConfigRef, bootstrapResource, g) 363 providerID := fakeInfrastructureRefReady(m.Spec.InfrastructureRef, infraResource, g) 364 fakeMachineNodeRef(&m, providerID, g) 365 } 366 367 // Verify that all Machines are Ready. 368 g.Eventually(func() int32 { 369 key := client.ObjectKey{Name: instance.Name, Namespace: instance.Namespace} 370 if err := env.Get(ctx, key, instance); err != nil { 371 return -1 372 } 373 return instance.Status.AvailableReplicas 374 }, timeout).Should(BeEquivalentTo(replicas)) 375 376 t.Log("Verifying MachineSet has MachinesCreatedCondition") 377 g.Eventually(func() bool { 378 key := client.ObjectKey{Name: instance.Name, Namespace: instance.Namespace} 379 if err := env.Get(ctx, key, instance); err != nil { 380 return false 381 } 382 return conditions.IsTrue(instance, clusterv1.MachinesCreatedCondition) 383 }, timeout).Should(BeTrue()) 384 385 t.Log("Verifying MachineSet has ResizedCondition") 386 g.Eventually(func() bool { 387 key := client.ObjectKey{Name: instance.Name, Namespace: instance.Namespace} 388 if err := env.Get(ctx, key, instance); err != nil { 389 return false 390 } 391 return conditions.IsTrue(instance, clusterv1.ResizedCondition) 392 }, timeout).Should(BeTrue()) 393 394 t.Log("Verifying MachineSet has MachinesReadyCondition") 395 g.Eventually(func() bool { 396 key := client.ObjectKey{Name: instance.Name, Namespace: instance.Namespace} 397 if err := env.Get(ctx, key, instance); err != nil { 398 return false 399 } 400 return conditions.IsTrue(instance, clusterv1.MachinesReadyCondition) 401 }, timeout).Should(BeTrue()) 402 403 // Validate that the controller set the cluster name label in selector. 404 g.Expect(instance.Status.Selector).To(ContainSubstring(testCluster.Name)) 405 }) 406 } 407 408 func TestMachineSetOwnerReference(t *testing.T) { 409 testCluster := &clusterv1.Cluster{ 410 TypeMeta: metav1.TypeMeta{Kind: "Cluster", APIVersion: clusterv1.GroupVersion.String()}, 411 ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: testClusterName}, 412 } 413 414 ms1 := newMachineSet("machineset1", "valid-cluster", int32(0)) 415 ms2 := newMachineSet("machineset2", "invalid-cluster", int32(0)) 416 ms3 := newMachineSet("machineset3", "valid-cluster", int32(0)) 417 ms3.OwnerReferences = []metav1.OwnerReference{ 418 { 419 APIVersion: clusterv1.GroupVersion.String(), 420 Kind: "MachineDeployment", 421 Name: "valid-machinedeployment", 422 }, 423 } 424 425 testCases := []struct { 426 name string 427 request reconcile.Request 428 ms *clusterv1.MachineSet 429 expectReconcileErr bool 430 expectedOR []metav1.OwnerReference 431 }{ 432 { 433 name: "should add cluster owner reference to machine set", 434 request: reconcile.Request{ 435 NamespacedName: util.ObjectKey(ms1), 436 }, 437 ms: ms1, 438 expectedOR: []metav1.OwnerReference{ 439 { 440 APIVersion: testCluster.APIVersion, 441 Kind: testCluster.Kind, 442 Name: testCluster.Name, 443 UID: testCluster.UID, 444 }, 445 }, 446 }, 447 { 448 name: "should not add cluster owner reference if machine is owned by a machine deployment", 449 request: reconcile.Request{ 450 NamespacedName: util.ObjectKey(ms3), 451 }, 452 ms: ms3, 453 expectedOR: []metav1.OwnerReference{ 454 { 455 APIVersion: clusterv1.GroupVersion.String(), 456 Kind: "MachineDeployment", 457 Name: "valid-machinedeployment", 458 }, 459 }, 460 }, 461 } 462 463 for _, tc := range testCases { 464 t.Run(tc.name, func(t *testing.T) { 465 g := NewWithT(t) 466 467 c := fake.NewClientBuilder().WithObjects( 468 testCluster, 469 ms1, 470 ms2, 471 ms3, 472 ).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 473 msr := &Reconciler{ 474 Client: c, 475 UnstructuredCachingClient: c, 476 recorder: record.NewFakeRecorder(32), 477 } 478 479 _, err := msr.Reconcile(ctx, tc.request) 480 if tc.expectReconcileErr { 481 g.Expect(err).To(HaveOccurred()) 482 } else { 483 g.Expect(err).ToNot(HaveOccurred()) 484 } 485 486 key := client.ObjectKey{Namespace: tc.ms.Namespace, Name: tc.ms.Name} 487 var actual clusterv1.MachineSet 488 if len(tc.expectedOR) > 0 { 489 g.Expect(msr.Client.Get(ctx, key, &actual)).To(Succeed()) 490 g.Expect(actual.OwnerReferences).To(BeComparableTo(tc.expectedOR)) 491 } else { 492 g.Expect(actual.OwnerReferences).To(BeEmpty()) 493 } 494 }) 495 } 496 } 497 498 func TestMachineSetReconcile(t *testing.T) { 499 testCluster := &clusterv1.Cluster{ 500 ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceDefault, Name: testClusterName}, 501 } 502 503 t.Run("ignore machine sets marked for deletion", func(t *testing.T) { 504 g := NewWithT(t) 505 506 dt := metav1.Now() 507 ms := &clusterv1.MachineSet{ 508 ObjectMeta: metav1.ObjectMeta{ 509 Name: "machineset1", 510 Namespace: metav1.NamespaceDefault, 511 DeletionTimestamp: &dt, 512 Finalizers: []string{"block-deletion"}, 513 }, 514 Spec: clusterv1.MachineSetSpec{ 515 ClusterName: testClusterName, 516 }, 517 } 518 request := reconcile.Request{ 519 NamespacedName: util.ObjectKey(ms), 520 } 521 522 c := fake.NewClientBuilder().WithObjects(testCluster, ms).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 523 msr := &Reconciler{ 524 Client: c, 525 UnstructuredCachingClient: c, 526 recorder: record.NewFakeRecorder(32), 527 } 528 result, err := msr.Reconcile(ctx, request) 529 g.Expect(err).ToNot(HaveOccurred()) 530 g.Expect(result).To(BeComparableTo(reconcile.Result{})) 531 }) 532 533 t.Run("records event if reconcile fails", func(t *testing.T) { 534 g := NewWithT(t) 535 536 ms := newMachineSet("machineset1", testClusterName, int32(0)) 537 ms.Spec.Selector.MatchLabels = map[string]string{ 538 "--$-invalid": "true", 539 } 540 541 request := reconcile.Request{ 542 NamespacedName: util.ObjectKey(ms), 543 } 544 545 rec := record.NewFakeRecorder(32) 546 c := fake.NewClientBuilder().WithObjects(testCluster, ms).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 547 msr := &Reconciler{ 548 Client: c, 549 UnstructuredCachingClient: c, 550 recorder: rec, 551 } 552 _, _ = msr.Reconcile(ctx, request) 553 g.Eventually(rec.Events).Should(Receive()) 554 }) 555 556 t.Run("reconcile successfully when labels are missing", func(t *testing.T) { 557 g := NewWithT(t) 558 559 ms := newMachineSet("machineset1", testClusterName, int32(0)) 560 ms.Labels = nil 561 ms.Spec.Selector.MatchLabels = nil 562 ms.Spec.Template.Labels = nil 563 564 request := reconcile.Request{ 565 NamespacedName: util.ObjectKey(ms), 566 } 567 568 rec := record.NewFakeRecorder(32) 569 c := fake.NewClientBuilder().WithObjects(testCluster, ms).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 570 msr := &Reconciler{ 571 Client: c, 572 UnstructuredCachingClient: c, 573 recorder: rec, 574 } 575 _, err := msr.Reconcile(ctx, request) 576 g.Expect(err).ToNot(HaveOccurred()) 577 }) 578 } 579 580 func TestMachineSetToMachines(t *testing.T) { 581 machineSetList := []client.Object{ 582 &clusterv1.MachineSet{ 583 ObjectMeta: metav1.ObjectMeta{ 584 Name: "withMatchingLabels", 585 Namespace: metav1.NamespaceDefault, 586 }, 587 Spec: clusterv1.MachineSetSpec{ 588 Selector: metav1.LabelSelector{ 589 MatchLabels: map[string]string{ 590 "foo": "bar", 591 clusterv1.ClusterNameLabel: testClusterName, 592 }, 593 }, 594 }, 595 }, 596 } 597 controller := true 598 m := clusterv1.Machine{ 599 ObjectMeta: metav1.ObjectMeta{ 600 Name: "withOwnerRef", 601 Namespace: metav1.NamespaceDefault, 602 Labels: map[string]string{ 603 clusterv1.ClusterNameLabel: testClusterName, 604 }, 605 OwnerReferences: []metav1.OwnerReference{ 606 { 607 Name: "Owner", 608 Kind: machineSetKind.Kind, 609 Controller: &controller, 610 }, 611 }, 612 }, 613 } 614 m2 := clusterv1.Machine{ 615 ObjectMeta: metav1.ObjectMeta{ 616 Name: "noOwnerRefNoLabels", 617 Namespace: metav1.NamespaceDefault, 618 Labels: map[string]string{ 619 clusterv1.ClusterNameLabel: testClusterName, 620 }, 621 }, 622 } 623 m3 := clusterv1.Machine{ 624 ObjectMeta: metav1.ObjectMeta{ 625 Name: "withMatchingLabels", 626 Namespace: metav1.NamespaceDefault, 627 Labels: map[string]string{ 628 "foo": "bar", 629 clusterv1.ClusterNameLabel: testClusterName, 630 }, 631 }, 632 } 633 testsCases := []struct { 634 name string 635 mapObject client.Object 636 expected []reconcile.Request 637 }{ 638 { 639 name: "should return empty request when controller is set", 640 mapObject: &m, 641 expected: []reconcile.Request{}, 642 }, 643 { 644 name: "should return nil if machine has no owner reference", 645 mapObject: &m2, 646 expected: nil, 647 }, 648 { 649 name: "should return request if machine set's labels matches machine's labels", 650 mapObject: &m3, 651 expected: []reconcile.Request{ 652 {NamespacedName: client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: "withMatchingLabels"}}, 653 }, 654 }, 655 } 656 657 c := fake.NewClientBuilder().WithObjects(append(machineSetList, &m, &m2, &m3)...).Build() 658 r := &Reconciler{ 659 Client: c, 660 UnstructuredCachingClient: c, 661 } 662 663 for _, tc := range testsCases { 664 t.Run(tc.name, func(t *testing.T) { 665 gs := NewWithT(t) 666 667 got := r.MachineToMachineSets(ctx, tc.mapObject) 668 gs.Expect(got).To(BeComparableTo(tc.expected)) 669 }) 670 } 671 } 672 673 func TestShouldExcludeMachine(t *testing.T) { 674 controller := true 675 testCases := []struct { 676 machineSet clusterv1.MachineSet 677 machine clusterv1.Machine 678 expected bool 679 }{ 680 { 681 machineSet: clusterv1.MachineSet{ 682 ObjectMeta: metav1.ObjectMeta{UID: "1"}, 683 }, 684 machine: clusterv1.Machine{ 685 ObjectMeta: metav1.ObjectMeta{ 686 Name: "withNoMatchingOwnerRef", 687 Namespace: metav1.NamespaceDefault, 688 OwnerReferences: []metav1.OwnerReference{ 689 { 690 Name: "Owner", 691 Kind: machineSetKind.Kind, 692 Controller: &controller, 693 UID: "not-1", 694 }, 695 }, 696 }, 697 }, 698 expected: true, 699 }, 700 { 701 machineSet: clusterv1.MachineSet{ 702 ObjectMeta: metav1.ObjectMeta{UID: "1"}, 703 }, 704 machine: clusterv1.Machine{ 705 ObjectMeta: metav1.ObjectMeta{ 706 Name: "withMatchingOwnerRef", 707 Namespace: metav1.NamespaceDefault, 708 OwnerReferences: []metav1.OwnerReference{ 709 { 710 Name: "Owner", 711 Kind: machineSetKind.Kind, 712 Controller: &controller, 713 UID: "1", 714 }, 715 }, 716 }, 717 }, 718 expected: false, 719 }, 720 { 721 machineSet: clusterv1.MachineSet{ 722 Spec: clusterv1.MachineSetSpec{ 723 Selector: metav1.LabelSelector{ 724 MatchLabels: map[string]string{ 725 "foo": "bar", 726 }, 727 }, 728 }, 729 }, 730 machine: clusterv1.Machine{ 731 ObjectMeta: metav1.ObjectMeta{ 732 Name: "withMatchingLabels", 733 Namespace: metav1.NamespaceDefault, 734 Labels: map[string]string{ 735 "foo": "bar", 736 }, 737 }, 738 }, 739 expected: false, 740 }, 741 { 742 machineSet: clusterv1.MachineSet{}, 743 machine: clusterv1.Machine{ 744 ObjectMeta: metav1.ObjectMeta{ 745 Name: "withDeletionTimestamp", 746 Namespace: metav1.NamespaceDefault, 747 DeletionTimestamp: &metav1.Time{Time: time.Now()}, 748 Labels: map[string]string{ 749 "foo": "bar", 750 }, 751 }, 752 }, 753 expected: false, 754 }, 755 } 756 757 for i := range testCases { 758 tc := testCases[i] 759 g := NewWithT(t) 760 761 got := shouldExcludeMachine(&tc.machineSet, &tc.machine) 762 763 g.Expect(got).To(Equal(tc.expected)) 764 } 765 } 766 767 func TestAdoptOrphan(t *testing.T) { 768 g := NewWithT(t) 769 770 m := clusterv1.Machine{ 771 ObjectMeta: metav1.ObjectMeta{ 772 Name: "orphanMachine", 773 }, 774 } 775 ms := clusterv1.MachineSet{ 776 ObjectMeta: metav1.ObjectMeta{ 777 Name: "adoptOrphanMachine", 778 }, 779 } 780 controller := true 781 blockOwnerDeletion := true 782 testCases := []struct { 783 machineSet clusterv1.MachineSet 784 machine clusterv1.Machine 785 expected []metav1.OwnerReference 786 }{ 787 { 788 machine: m, 789 machineSet: ms, 790 expected: []metav1.OwnerReference{ 791 { 792 APIVersion: clusterv1.GroupVersion.String(), 793 Kind: machineSetKind.Kind, 794 Name: "adoptOrphanMachine", 795 UID: "", 796 Controller: &controller, 797 BlockOwnerDeletion: &blockOwnerDeletion, 798 }, 799 }, 800 }, 801 } 802 803 c := fake.NewClientBuilder().WithObjects(&m).Build() 804 r := &Reconciler{ 805 Client: c, 806 UnstructuredCachingClient: c, 807 } 808 for i := range testCases { 809 tc := testCases[i] 810 g.Expect(r.adoptOrphan(ctx, tc.machineSet.DeepCopy(), tc.machine.DeepCopy())).To(Succeed()) 811 812 key := client.ObjectKey{Namespace: tc.machine.Namespace, Name: tc.machine.Name} 813 g.Expect(r.Client.Get(ctx, key, &tc.machine)).To(Succeed()) 814 815 got := tc.machine.GetOwnerReferences() 816 g.Expect(got).To(BeComparableTo(tc.expected)) 817 } 818 } 819 820 func newMachineSet(name, cluster string, replicas int32) *clusterv1.MachineSet { 821 return &clusterv1.MachineSet{ 822 ObjectMeta: metav1.ObjectMeta{ 823 Name: name, 824 Namespace: metav1.NamespaceDefault, 825 Labels: map[string]string{ 826 clusterv1.ClusterNameLabel: cluster, 827 }, 828 }, 829 Spec: clusterv1.MachineSetSpec{ 830 ClusterName: testClusterName, 831 Replicas: &replicas, 832 Template: clusterv1.MachineTemplateSpec{ 833 ObjectMeta: clusterv1.ObjectMeta{ 834 Labels: map[string]string{ 835 clusterv1.ClusterNameLabel: cluster, 836 }, 837 }, 838 }, 839 Selector: metav1.LabelSelector{ 840 MatchLabels: map[string]string{ 841 clusterv1.ClusterNameLabel: cluster, 842 }, 843 }, 844 }, 845 } 846 } 847 848 func TestMachineSetReconcile_MachinesCreatedConditionFalseOnBadInfraRef(t *testing.T) { 849 g := NewWithT(t) 850 replicas := int32(1) 851 version := "v1.21.0" 852 cluster := &clusterv1.Cluster{ 853 ObjectMeta: metav1.ObjectMeta{ 854 Name: "foo", 855 Namespace: metav1.NamespaceDefault, 856 }, 857 } 858 859 ms := &clusterv1.MachineSet{ 860 ObjectMeta: metav1.ObjectMeta{ 861 Name: "ms-foo", 862 Namespace: metav1.NamespaceDefault, 863 Labels: map[string]string{ 864 clusterv1.ClusterNameLabel: cluster.Name, 865 }, 866 }, 867 Spec: clusterv1.MachineSetSpec{ 868 ClusterName: cluster.ObjectMeta.Name, 869 Replicas: &replicas, 870 Template: clusterv1.MachineTemplateSpec{ 871 ObjectMeta: clusterv1.ObjectMeta{ 872 Labels: map[string]string{ 873 clusterv1.ClusterNameLabel: cluster.Name, 874 }, 875 }, 876 Spec: clusterv1.MachineSpec{ 877 InfrastructureRef: corev1.ObjectReference{ 878 Kind: builder.GenericInfrastructureMachineTemplateCRD.Kind, 879 APIVersion: builder.GenericInfrastructureMachineTemplateCRD.APIVersion, 880 // Try to break Infra Cloning 881 Name: "something_invalid", 882 Namespace: cluster.Namespace, 883 }, 884 Version: &version, 885 }, 886 }, 887 Selector: metav1.LabelSelector{ 888 MatchLabels: map[string]string{ 889 clusterv1.ClusterNameLabel: cluster.Name, 890 }, 891 }, 892 }, 893 } 894 895 key := util.ObjectKey(ms) 896 request := reconcile.Request{ 897 NamespacedName: key, 898 } 899 fakeClient := fake.NewClientBuilder().WithObjects(cluster, ms, builder.GenericInfrastructureMachineTemplateCRD.DeepCopy()).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 900 901 msr := &Reconciler{ 902 Client: fakeClient, 903 UnstructuredCachingClient: fakeClient, 904 recorder: record.NewFakeRecorder(32), 905 } 906 _, err := msr.Reconcile(ctx, request) 907 g.Expect(err).To(HaveOccurred()) 908 g.Expect(fakeClient.Get(ctx, key, ms)).To(Succeed()) 909 gotCond := conditions.Get(ms, clusterv1.MachinesCreatedCondition) 910 g.Expect(gotCond).ToNot(BeNil()) 911 g.Expect(gotCond.Status).To(Equal(corev1.ConditionFalse)) 912 g.Expect(gotCond.Reason).To(Equal(clusterv1.InfrastructureTemplateCloningFailedReason)) 913 } 914 915 func TestMachineSetReconciler_updateStatusResizedCondition(t *testing.T) { 916 cluster := &clusterv1.Cluster{ 917 ObjectMeta: metav1.ObjectMeta{ 918 Name: "foo", 919 Namespace: metav1.NamespaceDefault, 920 }, 921 } 922 923 testCases := []struct { 924 name string 925 machineSet *clusterv1.MachineSet 926 machines []*clusterv1.Machine 927 expectedReason string 928 expectedMessage string 929 }{ 930 { 931 name: "MachineSet should have ResizedCondition=false on scale up", 932 machineSet: newMachineSet("ms-scale-up", cluster.Name, int32(1)), 933 machines: []*clusterv1.Machine{}, 934 expectedReason: clusterv1.ScalingUpReason, 935 expectedMessage: "Scaling up MachineSet to 1 replicas (actual 0)", 936 }, 937 { 938 name: "MachineSet should have ResizedCondition=false on scale down", 939 machineSet: newMachineSet("ms-scale-down", cluster.Name, int32(0)), 940 machines: []*clusterv1.Machine{{ 941 ObjectMeta: metav1.ObjectMeta{ 942 Name: "machine-a", 943 Namespace: metav1.NamespaceDefault, 944 Labels: map[string]string{ 945 clusterv1.ClusterNameLabel: cluster.Name, 946 }, 947 }, 948 }, 949 }, 950 expectedReason: clusterv1.ScalingDownReason, 951 expectedMessage: "Scaling down MachineSet to 0 replicas (actual 1)", 952 }, 953 } 954 955 for _, tc := range testCases { 956 t.Run(tc.name, func(t *testing.T) { 957 g := NewWithT(t) 958 959 c := fake.NewClientBuilder().WithObjects().Build() 960 msr := &Reconciler{ 961 Client: c, 962 UnstructuredCachingClient: c, 963 recorder: record.NewFakeRecorder(32), 964 } 965 err := msr.updateStatus(ctx, cluster, tc.machineSet, tc.machines) 966 g.Expect(err).ToNot(HaveOccurred()) 967 gotCond := conditions.Get(tc.machineSet, clusterv1.ResizedCondition) 968 g.Expect(gotCond).ToNot(BeNil()) 969 g.Expect(gotCond.Status).To(Equal(corev1.ConditionFalse)) 970 g.Expect(gotCond.Reason).To(Equal(tc.expectedReason)) 971 g.Expect(gotCond.Message).To(Equal(tc.expectedMessage)) 972 }) 973 } 974 } 975 976 func TestMachineSetReconciler_syncMachines(t *testing.T) { 977 setup := func(t *testing.T, g *WithT) (*corev1.Namespace, *clusterv1.Cluster) { 978 t.Helper() 979 980 t.Log("Creating the namespace") 981 ns, err := env.CreateNamespace(ctx, "test-machine-set-reconciler-sync-machines") 982 g.Expect(err).ToNot(HaveOccurred()) 983 984 t.Log("Creating the Cluster") 985 cluster := &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: ns.Name, Name: testClusterName}} 986 g.Expect(env.Create(ctx, cluster)).To(Succeed()) 987 988 t.Log("Creating the Cluster Kubeconfig Secret") 989 g.Expect(env.CreateKubeconfigSecret(ctx, cluster)).To(Succeed()) 990 991 return ns, cluster 992 } 993 994 teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace, cluster *clusterv1.Cluster) { 995 t.Helper() 996 997 t.Log("Deleting the Cluster") 998 g.Expect(env.Delete(ctx, cluster)).To(Succeed()) 999 t.Log("Deleting the namespace") 1000 g.Expect(env.Delete(ctx, ns)).To(Succeed()) 1001 } 1002 1003 g := NewWithT(t) 1004 namespace, testCluster := setup(t, g) 1005 defer teardown(t, g, namespace, testCluster) 1006 1007 classicManager := "manager" 1008 replicas := int32(2) 1009 version := "v1.25.3" 1010 duration10s := &metav1.Duration{Duration: 10 * time.Second} 1011 ms := &clusterv1.MachineSet{ 1012 ObjectMeta: metav1.ObjectMeta{ 1013 UID: "abc-123-ms-uid", 1014 Name: "ms-1", 1015 Namespace: namespace.Name, 1016 Labels: map[string]string{ 1017 "label-1": "true", 1018 clusterv1.MachineDeploymentNameLabel: "md-1", 1019 }, 1020 }, 1021 Spec: clusterv1.MachineSetSpec{ 1022 ClusterName: testCluster.Name, 1023 Replicas: &replicas, 1024 Selector: metav1.LabelSelector{ 1025 MatchLabels: map[string]string{ 1026 "preserved-label": "preserved-value", 1027 }, 1028 }, 1029 Template: clusterv1.MachineTemplateSpec{ 1030 ObjectMeta: clusterv1.ObjectMeta{ 1031 Labels: map[string]string{ 1032 "preserved-label": "preserved-value", // Label will be preserved while testing in-place mutation. 1033 "dropped-label": "dropped-value", // Label will be dropped while testing in-place mutation. 1034 "modified-label": "modified-value", // Label value will be modified while testing in-place mutation. 1035 }, 1036 Annotations: map[string]string{ 1037 "preserved-annotation": "preserved-value", // Annotation will be preserved while testing in-place mutation. 1038 "dropped-annotation": "dropped-value", // Annotation will be dropped while testing in-place mutation. 1039 "modified-annotation": "modified-value", // Annotation value will be modified while testing in-place mutation. 1040 }, 1041 }, 1042 Spec: clusterv1.MachineSpec{ 1043 ClusterName: testCluster.Name, 1044 Version: &version, 1045 Bootstrap: clusterv1.Bootstrap{ 1046 ConfigRef: &corev1.ObjectReference{ 1047 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1048 Kind: "GenericBootstrapConfigTemplate", 1049 Name: "ms-template", 1050 }, 1051 }, 1052 InfrastructureRef: corev1.ObjectReference{ 1053 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 1054 Kind: "GenericInfrastructureMachineTemplate", 1055 Name: "ms-template", 1056 }, 1057 }, 1058 }, 1059 }, 1060 } 1061 1062 infraMachineSpec := map[string]interface{}{ 1063 "infra-field": "infra-value", 1064 } 1065 infraMachine := &unstructured.Unstructured{ 1066 Object: map[string]interface{}{ 1067 "kind": "GenericInfrastructureMachine", 1068 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 1069 "metadata": map[string]interface{}{ 1070 "name": "infra-machine-1", 1071 "namespace": namespace.Name, 1072 "labels": map[string]string{ 1073 "preserved-label": "preserved-value", 1074 "dropped-label": "dropped-value", 1075 "modified-label": "modified-value", 1076 }, 1077 "annotations": map[string]string{ 1078 "preserved-annotation": "preserved-value", 1079 "dropped-annotation": "dropped-value", 1080 "modified-annotation": "modified-value", 1081 }, 1082 }, 1083 "spec": infraMachineSpec, 1084 }, 1085 } 1086 g.Expect(env.Create(ctx, infraMachine, client.FieldOwner(classicManager))).To(Succeed()) 1087 1088 bootstrapConfigSpec := map[string]interface{}{ 1089 "bootstrap-field": "bootstrap-value", 1090 } 1091 bootstrapConfig := &unstructured.Unstructured{ 1092 Object: map[string]interface{}{ 1093 "kind": "GenericBootstrapConfig", 1094 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1095 "metadata": map[string]interface{}{ 1096 "name": "bootstrap-config-1", 1097 "namespace": namespace.Name, 1098 "labels": map[string]string{ 1099 "preserved-label": "preserved-value", 1100 "dropped-label": "dropped-value", 1101 "modified-label": "modified-value", 1102 }, 1103 "annotations": map[string]string{ 1104 "preserved-annotation": "preserved-value", 1105 "dropped-annotation": "dropped-value", 1106 "modified-annotation": "modified-value", 1107 }, 1108 }, 1109 "spec": bootstrapConfigSpec, 1110 }, 1111 } 1112 g.Expect(env.Create(ctx, bootstrapConfig, client.FieldOwner(classicManager))).To(Succeed()) 1113 1114 inPlaceMutatingMachine := &clusterv1.Machine{ 1115 TypeMeta: metav1.TypeMeta{ 1116 APIVersion: clusterv1.GroupVersion.String(), 1117 Kind: "Machine", 1118 }, 1119 ObjectMeta: metav1.ObjectMeta{ 1120 UID: "abc-123-uid", 1121 Name: "in-place-mutating-machine", 1122 Namespace: namespace.Name, 1123 Labels: map[string]string{ 1124 "preserved-label": "preserved-value", 1125 "dropped-label": "dropped-value", 1126 "modified-label": "modified-value", 1127 }, 1128 Annotations: map[string]string{ 1129 "preserved-annotation": "preserved-value", 1130 "dropped-annotation": "dropped-value", 1131 "modified-annotation": "modified-value", 1132 }, 1133 }, 1134 Spec: clusterv1.MachineSpec{ 1135 ClusterName: testClusterName, 1136 InfrastructureRef: corev1.ObjectReference{ 1137 Namespace: infraMachine.GetNamespace(), 1138 Name: infraMachine.GetName(), 1139 UID: infraMachine.GetUID(), 1140 APIVersion: infraMachine.GetAPIVersion(), 1141 Kind: infraMachine.GetKind(), 1142 }, 1143 Bootstrap: clusterv1.Bootstrap{ 1144 ConfigRef: &corev1.ObjectReference{ 1145 Namespace: bootstrapConfig.GetNamespace(), 1146 Name: bootstrapConfig.GetName(), 1147 UID: bootstrapConfig.GetUID(), 1148 APIVersion: bootstrapConfig.GetAPIVersion(), 1149 Kind: bootstrapConfig.GetKind(), 1150 }, 1151 }, 1152 }, 1153 } 1154 g.Expect(env.Create(ctx, inPlaceMutatingMachine, client.FieldOwner(classicManager))).To(Succeed()) 1155 1156 deletingMachine := &clusterv1.Machine{ 1157 TypeMeta: metav1.TypeMeta{ 1158 APIVersion: clusterv1.GroupVersion.String(), 1159 Kind: "Machine", 1160 }, 1161 ObjectMeta: metav1.ObjectMeta{ 1162 UID: "abc-123-uid", 1163 Name: "deleting-machine", 1164 Namespace: namespace.Name, 1165 Labels: map[string]string{}, 1166 Annotations: map[string]string{}, 1167 Finalizers: []string{"testing-finalizer"}, 1168 }, 1169 Spec: clusterv1.MachineSpec{ 1170 ClusterName: testClusterName, 1171 InfrastructureRef: corev1.ObjectReference{ 1172 Namespace: namespace.Name, 1173 }, 1174 Bootstrap: clusterv1.Bootstrap{ 1175 DataSecretName: ptr.To("machine-bootstrap-secret"), 1176 }, 1177 }, 1178 } 1179 g.Expect(env.Create(ctx, deletingMachine, client.FieldOwner(classicManager))).To(Succeed()) 1180 // Delete the machine to put it in the deleting state 1181 g.Expect(env.Delete(ctx, deletingMachine)).To(Succeed()) 1182 // Wait till the machine is marked for deletion 1183 g.Eventually(func() bool { 1184 if err := env.Get(ctx, client.ObjectKeyFromObject(deletingMachine), deletingMachine); err != nil { 1185 return false 1186 } 1187 return !deletingMachine.DeletionTimestamp.IsZero() 1188 }, timeout).Should(BeTrue()) 1189 1190 machines := []*clusterv1.Machine{inPlaceMutatingMachine, deletingMachine} 1191 1192 // 1193 // Verify Managed Fields 1194 // 1195 1196 // Run syncMachines to clean up managed fields and have proper field ownership 1197 // for Machines, InfrastructureMachines and BootstrapConfigs. 1198 reconciler := &Reconciler{ 1199 Client: env, 1200 UnstructuredCachingClient: env, 1201 ssaCache: ssa.NewCache(), 1202 } 1203 g.Expect(reconciler.syncMachines(ctx, ms, machines)).To(Succeed()) 1204 1205 // The inPlaceMutatingMachine should have cleaned up managed fields. 1206 updatedInPlaceMutatingMachine := inPlaceMutatingMachine.DeepCopy() 1207 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedInPlaceMutatingMachine), updatedInPlaceMutatingMachine)).To(Succeed()) 1208 // Verify ManagedFields 1209 g.Expect(updatedInPlaceMutatingMachine.ManagedFields).Should( 1210 ContainElement(ssa.MatchManagedFieldsEntry(machineSetManagerName, metav1.ManagedFieldsOperationApply)), 1211 "in-place mutable machine should contain an entry for SSA manager", 1212 ) 1213 g.Expect(updatedInPlaceMutatingMachine.ManagedFields).ShouldNot( 1214 ContainElement(ssa.MatchManagedFieldsEntry(classicManager, metav1.ManagedFieldsOperationUpdate)), 1215 "in-place mutable machine should not contain an entry for old manager", 1216 ) 1217 1218 // The InfrastructureMachine should have ownership of "labels" and "annotations" transferred to 1219 // "capi-machineset" manager. 1220 updatedInfraMachine := infraMachine.DeepCopy() 1221 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedInfraMachine), updatedInfraMachine)).To(Succeed()) 1222 1223 // Verify ManagedFields 1224 g.Expect(updatedInfraMachine.GetManagedFields()).Should( 1225 ssa.MatchFieldOwnership(machineSetManagerName, metav1.ManagedFieldsOperationApply, contract.Path{"f:metadata", "f:labels"})) 1226 g.Expect(updatedInfraMachine.GetManagedFields()).Should( 1227 ssa.MatchFieldOwnership(machineSetManagerName, metav1.ManagedFieldsOperationApply, contract.Path{"f:metadata", "f:annotations"})) 1228 g.Expect(updatedInfraMachine.GetManagedFields()).ShouldNot( 1229 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:metadata", "f:labels"})) 1230 g.Expect(updatedInfraMachine.GetManagedFields()).ShouldNot( 1231 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:metadata", "f:annotations"})) 1232 g.Expect(updatedInfraMachine.GetManagedFields()).Should( 1233 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:spec"})) 1234 1235 // The BootstrapConfig should have ownership of "labels" and "annotations" transferred to 1236 // "capi-machineset" manager. 1237 updatedBootstrapConfig := bootstrapConfig.DeepCopy() 1238 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedBootstrapConfig), updatedBootstrapConfig)).To(Succeed()) 1239 1240 // Verify ManagedFields 1241 g.Expect(updatedBootstrapConfig.GetManagedFields()).Should( 1242 ssa.MatchFieldOwnership(machineSetManagerName, metav1.ManagedFieldsOperationApply, contract.Path{"f:metadata", "f:labels"})) 1243 g.Expect(updatedBootstrapConfig.GetManagedFields()).Should( 1244 ssa.MatchFieldOwnership(machineSetManagerName, metav1.ManagedFieldsOperationApply, contract.Path{"f:metadata", "f:annotations"})) 1245 g.Expect(updatedBootstrapConfig.GetManagedFields()).ShouldNot( 1246 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:metadata", "f:labels"})) 1247 g.Expect(updatedBootstrapConfig.GetManagedFields()).ShouldNot( 1248 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:metadata", "f:annotations"})) 1249 g.Expect(updatedBootstrapConfig.GetManagedFields()).Should( 1250 ssa.MatchFieldOwnership(classicManager, metav1.ManagedFieldsOperationUpdate, contract.Path{"f:spec"})) 1251 1252 // 1253 // Verify In-place mutating fields 1254 // 1255 1256 // Update the MachineSet and verify the in-mutating fields are propagated. 1257 ms.Spec.Template.Labels = map[string]string{ 1258 "preserved-label": "preserved-value", // Keep the label and value as is 1259 "modified-label": "modified-value-2", // Modify the value of the label 1260 // Drop "dropped-label" 1261 } 1262 expectedLabels := map[string]string{ 1263 "preserved-label": "preserved-value", 1264 "modified-label": "modified-value-2", 1265 clusterv1.MachineSetNameLabel: ms.Name, 1266 clusterv1.MachineDeploymentNameLabel: "md-1", 1267 clusterv1.ClusterNameLabel: testClusterName, // This label is added by the Machine controller. 1268 } 1269 ms.Spec.Template.Annotations = map[string]string{ 1270 "preserved-annotation": "preserved-value", // Keep the annotation and value as is 1271 "modified-annotation": "modified-value-2", // Modify the value of the annotation 1272 // Drop "dropped-annotation" 1273 } 1274 ms.Spec.Template.Spec.NodeDrainTimeout = duration10s 1275 ms.Spec.Template.Spec.NodeDeletionTimeout = duration10s 1276 ms.Spec.Template.Spec.NodeVolumeDetachTimeout = duration10s 1277 g.Expect(reconciler.syncMachines(ctx, ms, []*clusterv1.Machine{updatedInPlaceMutatingMachine, deletingMachine})).To(Succeed()) 1278 1279 // Verify in-place mutable fields are updated on the Machine. 1280 updatedInPlaceMutatingMachine = inPlaceMutatingMachine.DeepCopy() 1281 g.Eventually(func(g Gomega) { 1282 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedInPlaceMutatingMachine), updatedInPlaceMutatingMachine)).To(Succeed()) 1283 // Verify Labels 1284 g.Expect(updatedInPlaceMutatingMachine.Labels).Should(Equal(expectedLabels)) 1285 // Verify Annotations 1286 g.Expect(updatedInPlaceMutatingMachine.Annotations).Should(Equal(ms.Spec.Template.Annotations)) 1287 // Verify Node timeout values 1288 g.Expect(updatedInPlaceMutatingMachine.Spec.NodeDrainTimeout).Should(And( 1289 Not(BeNil()), 1290 HaveValue(Equal(*ms.Spec.Template.Spec.NodeDrainTimeout)), 1291 )) 1292 g.Expect(updatedInPlaceMutatingMachine.Spec.NodeDeletionTimeout).Should(And( 1293 Not(BeNil()), 1294 HaveValue(Equal(*ms.Spec.Template.Spec.NodeDeletionTimeout)), 1295 )) 1296 g.Expect(updatedInPlaceMutatingMachine.Spec.NodeVolumeDetachTimeout).Should(And( 1297 Not(BeNil()), 1298 HaveValue(Equal(*ms.Spec.Template.Spec.NodeVolumeDetachTimeout)), 1299 )) 1300 }, timeout).Should(Succeed()) 1301 1302 // Verify in-place mutable fields are updated on InfrastructureMachine 1303 updatedInfraMachine = infraMachine.DeepCopy() 1304 g.Eventually(func(g Gomega) { 1305 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedInfraMachine), updatedInfraMachine)).To(Succeed()) 1306 // Verify Labels 1307 g.Expect(updatedInfraMachine.GetLabels()).Should(Equal(expectedLabels)) 1308 // Verify Annotations 1309 g.Expect(updatedInfraMachine.GetAnnotations()).Should(Equal(ms.Spec.Template.Annotations)) 1310 // Verify spec remains the same 1311 g.Expect(updatedInfraMachine.Object).Should(HaveKeyWithValue("spec", infraMachineSpec)) 1312 }, timeout).Should(Succeed()) 1313 1314 // Verify in-place mutable fields are updated on the BootstrapConfig. 1315 updatedBootstrapConfig = bootstrapConfig.DeepCopy() 1316 g.Eventually(func(g Gomega) { 1317 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedBootstrapConfig), updatedBootstrapConfig)).To(Succeed()) 1318 // Verify Labels 1319 g.Expect(updatedBootstrapConfig.GetLabels()).Should(Equal(expectedLabels)) 1320 // Verify Annotations 1321 g.Expect(updatedBootstrapConfig.GetAnnotations()).Should(Equal(ms.Spec.Template.Annotations)) 1322 // Verify spec remains the same 1323 g.Expect(updatedBootstrapConfig.Object).Should(HaveKeyWithValue("spec", bootstrapConfigSpec)) 1324 }, timeout).Should(Succeed()) 1325 1326 // Wait to ensure Machine is not updated. 1327 // Verify that the machine stays the same consistently. 1328 g.Consistently(func(g Gomega) { 1329 // The deleting machine should not change. 1330 updatedDeletingMachine := deletingMachine.DeepCopy() 1331 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(updatedDeletingMachine), updatedDeletingMachine)).To(Succeed()) 1332 1333 // Verify ManagedFields 1334 g.Expect(updatedDeletingMachine.ManagedFields).ShouldNot( 1335 ContainElement(ssa.MatchManagedFieldsEntry(machineSetManagerName, metav1.ManagedFieldsOperationApply)), 1336 "deleting machine should not contain an entry for SSA manager", 1337 ) 1338 g.Expect(updatedDeletingMachine.ManagedFields).Should( 1339 ContainElement(ssa.MatchManagedFieldsEntry("manager", metav1.ManagedFieldsOperationUpdate)), 1340 "in-place mutable machine should still contain an entry for old manager", 1341 ) 1342 1343 // Verify in-place mutable fields are still the same. 1344 g.Expect(updatedDeletingMachine.Labels).Should(Equal(deletingMachine.Labels)) 1345 g.Expect(updatedDeletingMachine.Annotations).Should(Equal(deletingMachine.Annotations)) 1346 g.Expect(updatedDeletingMachine.Spec.NodeDrainTimeout).Should(Equal(deletingMachine.Spec.NodeDrainTimeout)) 1347 g.Expect(updatedDeletingMachine.Spec.NodeDeletionTimeout).Should(Equal(deletingMachine.Spec.NodeDeletionTimeout)) 1348 g.Expect(updatedDeletingMachine.Spec.NodeVolumeDetachTimeout).Should(Equal(deletingMachine.Spec.NodeVolumeDetachTimeout)) 1349 }, 5*time.Second).Should(Succeed()) 1350 } 1351 1352 func TestMachineSetReconciler_reconcileUnhealthyMachines(t *testing.T) { 1353 t.Run("should delete unhealthy machines if preflight checks pass", func(t *testing.T) { 1354 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachineSetPreflightChecks, true)() 1355 1356 g := NewWithT(t) 1357 1358 controlPlaneStable := builder.ControlPlane("default", "cp1"). 1359 WithVersion("v1.26.2"). 1360 WithStatusFields(map[string]interface{}{ 1361 "status.version": "v1.26.2", 1362 }). 1363 Build() 1364 cluster := &clusterv1.Cluster{ 1365 ObjectMeta: metav1.ObjectMeta{ 1366 Name: "test-cluster", 1367 Namespace: "default", 1368 }, 1369 Spec: clusterv1.ClusterSpec{ 1370 ControlPlaneRef: contract.ObjToRef(controlPlaneStable), 1371 }, 1372 } 1373 machineSet := &clusterv1.MachineSet{} 1374 1375 unhealthyMachine := &clusterv1.Machine{ 1376 ObjectMeta: metav1.ObjectMeta{ 1377 Name: "unhealthy-machine", 1378 Namespace: "default", 1379 }, 1380 Status: clusterv1.MachineStatus{ 1381 Conditions: []clusterv1.Condition{ 1382 { 1383 Type: clusterv1.MachineOwnerRemediatedCondition, 1384 Status: corev1.ConditionFalse, 1385 }, 1386 }, 1387 }, 1388 } 1389 healthyMachine := &clusterv1.Machine{ 1390 ObjectMeta: metav1.ObjectMeta{ 1391 Name: "healthy-machine", 1392 Namespace: "default", 1393 }, 1394 } 1395 1396 machines := []*clusterv1.Machine{unhealthyMachine, healthyMachine} 1397 1398 fakeClient := fake.NewClientBuilder().WithObjects(controlPlaneStable, unhealthyMachine, healthyMachine).Build() 1399 r := &Reconciler{ 1400 Client: fakeClient, 1401 UnstructuredCachingClient: fakeClient, 1402 } 1403 _, err := r.reconcileUnhealthyMachines(ctx, cluster, machineSet, machines) 1404 g.Expect(err).ToNot(HaveOccurred()) 1405 // Verify the unhealthy machine is deleted. 1406 m := &clusterv1.Machine{} 1407 err = r.Client.Get(ctx, client.ObjectKeyFromObject(unhealthyMachine), m) 1408 g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) 1409 // Verify the healthy machine is not deleted. 1410 m = &clusterv1.Machine{} 1411 g.Expect(r.Client.Get(ctx, client.ObjectKeyFromObject(healthyMachine), m)).Should(Succeed()) 1412 }) 1413 1414 t.Run("should update the unhealthy machine MachineOwnerRemediated condition if preflight checks did not pass", func(t *testing.T) { 1415 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachineSetPreflightChecks, true)() 1416 1417 g := NewWithT(t) 1418 1419 // An upgrading control plane should cause the preflight checks to not pass. 1420 controlPlaneUpgrading := builder.ControlPlane("default", "cp1"). 1421 WithVersion("v1.26.2"). 1422 WithStatusFields(map[string]interface{}{ 1423 "status.version": "v1.25.2", 1424 }). 1425 Build() 1426 cluster := &clusterv1.Cluster{ 1427 ObjectMeta: metav1.ObjectMeta{ 1428 Name: "test-cluster", 1429 Namespace: "default", 1430 }, 1431 Spec: clusterv1.ClusterSpec{ 1432 ControlPlaneRef: contract.ObjToRef(controlPlaneUpgrading), 1433 }, 1434 } 1435 machineSet := &clusterv1.MachineSet{} 1436 1437 unhealthyMachine := &clusterv1.Machine{ 1438 ObjectMeta: metav1.ObjectMeta{ 1439 Name: "unhealthy-machine", 1440 Namespace: "default", 1441 }, 1442 Status: clusterv1.MachineStatus{ 1443 Conditions: []clusterv1.Condition{ 1444 { 1445 Type: clusterv1.MachineOwnerRemediatedCondition, 1446 Status: corev1.ConditionFalse, 1447 }, 1448 }, 1449 }, 1450 } 1451 healthyMachine := &clusterv1.Machine{ 1452 ObjectMeta: metav1.ObjectMeta{ 1453 Name: "healthy-machine", 1454 Namespace: "default", 1455 }, 1456 } 1457 1458 machines := []*clusterv1.Machine{unhealthyMachine, healthyMachine} 1459 fakeClient := fake.NewClientBuilder().WithObjects(controlPlaneUpgrading, unhealthyMachine, healthyMachine).WithStatusSubresource(&clusterv1.Machine{}).Build() 1460 r := &Reconciler{ 1461 Client: fakeClient, 1462 UnstructuredCachingClient: fakeClient, 1463 } 1464 _, err := r.reconcileUnhealthyMachines(ctx, cluster, machineSet, machines) 1465 g.Expect(err).ToNot(HaveOccurred()) 1466 1467 // Verify the unhealthy machine has the updated condition. 1468 condition := clusterv1.MachineOwnerRemediatedCondition 1469 m := &clusterv1.Machine{} 1470 g.Expect(r.Client.Get(ctx, client.ObjectKeyFromObject(unhealthyMachine), m)).To(Succeed()) 1471 g.Expect(conditions.Has(m, condition)). 1472 To(BeTrue(), "Machine should have the %s condition set", condition) 1473 machineOwnerRemediatedCondition := conditions.Get(m, condition) 1474 g.Expect(machineOwnerRemediatedCondition.Status). 1475 To(Equal(corev1.ConditionFalse), "%s condition status should be false", condition) 1476 g.Expect(machineOwnerRemediatedCondition.Reason). 1477 To(Equal(clusterv1.WaitingForRemediationReason), "%s condition should have reason %s", condition, clusterv1.WaitingForRemediationReason) 1478 1479 // Verify the healthy machine continues to not have the MachineOwnerRemediated condition. 1480 m = &clusterv1.Machine{} 1481 g.Expect(r.Client.Get(ctx, client.ObjectKeyFromObject(healthyMachine), m)).To(Succeed()) 1482 g.Expect(conditions.Has(m, condition)). 1483 To(BeFalse(), "Machine should not have the %s condition set", condition) 1484 }) 1485 } 1486 1487 func TestMachineSetReconciler_syncReplicas(t *testing.T) { 1488 t.Run("should hold off on creating new machines when preflight checks do not pass", func(t *testing.T) { 1489 defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.MachineSetPreflightChecks, true)() 1490 1491 g := NewWithT(t) 1492 1493 // An upgrading control plane should cause the preflight checks to not pass. 1494 controlPlaneUpgrading := builder.ControlPlane("default", "test-cp"). 1495 WithVersion("v1.26.2"). 1496 WithStatusFields(map[string]interface{}{ 1497 "status.version": "v1.25.2", 1498 }). 1499 Build() 1500 cluster := &clusterv1.Cluster{ 1501 ObjectMeta: metav1.ObjectMeta{ 1502 Name: "test-cluster", 1503 Namespace: "default", 1504 }, 1505 Spec: clusterv1.ClusterSpec{ 1506 ControlPlaneRef: contract.ObjToRef(controlPlaneUpgrading), 1507 }, 1508 } 1509 machineSet := &clusterv1.MachineSet{ 1510 ObjectMeta: metav1.ObjectMeta{ 1511 Name: "test-machineset", 1512 Namespace: "default", 1513 }, 1514 Spec: clusterv1.MachineSetSpec{ 1515 Replicas: ptr.To[int32](1), 1516 }, 1517 } 1518 1519 fakeClient := fake.NewClientBuilder().WithObjects(controlPlaneUpgrading, machineSet).WithStatusSubresource(&clusterv1.MachineSet{}).Build() 1520 r := &Reconciler{ 1521 Client: fakeClient, 1522 UnstructuredCachingClient: fakeClient, 1523 } 1524 result, err := r.syncReplicas(ctx, cluster, machineSet, nil) 1525 g.Expect(err).ToNot(HaveOccurred()) 1526 g.Expect(result.IsZero()).To(BeFalse(), "syncReplicas should not return a 'zero' result") 1527 1528 // Verify the proper condition is set on the MachineSet. 1529 condition := clusterv1.MachinesCreatedCondition 1530 g.Expect(conditions.Has(machineSet, condition)). 1531 To(BeTrue(), "MachineSet should have the %s condition set", condition) 1532 machinesCreatedCondition := conditions.Get(machineSet, condition) 1533 g.Expect(machinesCreatedCondition.Status). 1534 To(Equal(corev1.ConditionFalse), "%s condition status should be %s", condition, corev1.ConditionFalse) 1535 g.Expect(machinesCreatedCondition.Reason). 1536 To(Equal(clusterv1.PreflightCheckFailedReason), "%s condition reason should be %s", condition, clusterv1.PreflightCheckFailedReason) 1537 1538 // Verify no new Machines are created. 1539 machineList := &clusterv1.MachineList{} 1540 g.Expect(r.Client.List(ctx, machineList)).To(Succeed()) 1541 g.Expect(machineList.Items).To(BeEmpty(), "There should not be any machines") 1542 }) 1543 } 1544 1545 func TestComputeDesiredMachine(t *testing.T) { 1546 duration5s := &metav1.Duration{Duration: 5 * time.Second} 1547 duration10s := &metav1.Duration{Duration: 10 * time.Second} 1548 1549 infraRef := corev1.ObjectReference{ 1550 Kind: "GenericInfrastructureMachineTemplate", 1551 Name: "infra-template-1", 1552 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 1553 } 1554 bootstrapRef := corev1.ObjectReference{ 1555 Kind: "GenericBootstrapConfigTemplate", 1556 Name: "bootstrap-template-1", 1557 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1558 } 1559 1560 ms := &clusterv1.MachineSet{ 1561 ObjectMeta: metav1.ObjectMeta{ 1562 Namespace: "default", 1563 Name: "ms1", 1564 Labels: map[string]string{ 1565 clusterv1.MachineDeploymentNameLabel: "md1", 1566 }, 1567 }, 1568 Spec: clusterv1.MachineSetSpec{ 1569 ClusterName: "test-cluster", 1570 Replicas: ptr.To[int32](3), 1571 MinReadySeconds: 10, 1572 Selector: metav1.LabelSelector{ 1573 MatchLabels: map[string]string{"k1": "v1"}, 1574 }, 1575 Template: clusterv1.MachineTemplateSpec{ 1576 ObjectMeta: clusterv1.ObjectMeta{ 1577 Labels: map[string]string{"machine-label1": "machine-value1"}, 1578 Annotations: map[string]string{"machine-annotation1": "machine-value1"}, 1579 }, 1580 Spec: clusterv1.MachineSpec{ 1581 Version: ptr.To("v1.25.3"), 1582 InfrastructureRef: infraRef, 1583 Bootstrap: clusterv1.Bootstrap{ 1584 ConfigRef: &bootstrapRef, 1585 }, 1586 NodeDrainTimeout: duration10s, 1587 NodeVolumeDetachTimeout: duration10s, 1588 NodeDeletionTimeout: duration10s, 1589 }, 1590 }, 1591 }, 1592 } 1593 1594 skeletonMachine := &clusterv1.Machine{ 1595 ObjectMeta: metav1.ObjectMeta{ 1596 Namespace: "default", 1597 Labels: map[string]string{ 1598 "machine-label1": "machine-value1", 1599 clusterv1.MachineSetNameLabel: "ms1", 1600 clusterv1.MachineDeploymentNameLabel: "md1", 1601 }, 1602 Annotations: map[string]string{"machine-annotation1": "machine-value1"}, 1603 Finalizers: []string{clusterv1.MachineFinalizer}, 1604 }, 1605 Spec: clusterv1.MachineSpec{ 1606 ClusterName: "test-cluster", 1607 Version: ptr.To("v1.25.3"), 1608 NodeDrainTimeout: duration10s, 1609 NodeVolumeDetachTimeout: duration10s, 1610 NodeDeletionTimeout: duration10s, 1611 }, 1612 } 1613 1614 // Creating a new Machine 1615 expectedNewMachine := skeletonMachine.DeepCopy() 1616 1617 // Updating an existing Machine 1618 existingMachine := skeletonMachine.DeepCopy() 1619 existingMachine.Name = "exiting-machine-1" 1620 existingMachine.UID = "abc-123-existing-machine-1" 1621 existingMachine.Labels = nil 1622 existingMachine.Annotations = nil 1623 existingMachine.Spec.InfrastructureRef = corev1.ObjectReference{ 1624 Kind: "GenericInfrastructureMachine", 1625 Name: "infra-machine-1", 1626 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 1627 } 1628 existingMachine.Spec.Bootstrap.ConfigRef = &corev1.ObjectReference{ 1629 Kind: "GenericBootstrapConfig", 1630 Name: "bootstrap-config-1", 1631 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1632 } 1633 existingMachine.Spec.NodeDrainTimeout = duration5s 1634 existingMachine.Spec.NodeDeletionTimeout = duration5s 1635 existingMachine.Spec.NodeVolumeDetachTimeout = duration5s 1636 1637 expectedUpdatedMachine := skeletonMachine.DeepCopy() 1638 expectedUpdatedMachine.Name = existingMachine.Name 1639 expectedUpdatedMachine.UID = existingMachine.UID 1640 expectedUpdatedMachine.Spec.InfrastructureRef = *existingMachine.Spec.InfrastructureRef.DeepCopy() 1641 expectedUpdatedMachine.Spec.Bootstrap.ConfigRef = existingMachine.Spec.Bootstrap.ConfigRef.DeepCopy() 1642 1643 tests := []struct { 1644 name string 1645 existingMachine *clusterv1.Machine 1646 want *clusterv1.Machine 1647 }{ 1648 { 1649 name: "creating a new Machine", 1650 existingMachine: nil, 1651 want: expectedNewMachine, 1652 }, 1653 { 1654 name: "updating an existing Machine", 1655 existingMachine: existingMachine, 1656 want: expectedUpdatedMachine, 1657 }, 1658 } 1659 1660 for _, tt := range tests { 1661 t.Run(tt.name, func(t *testing.T) { 1662 g := NewWithT(t) 1663 got := (&Reconciler{}).computeDesiredMachine(ms, tt.existingMachine) 1664 assertMachine(g, got, tt.want) 1665 }) 1666 } 1667 } 1668 1669 func assertMachine(g *WithT, actualMachine *clusterv1.Machine, expectedMachine *clusterv1.Machine) { 1670 // Check Name 1671 if expectedMachine.Name != "" { 1672 g.Expect(actualMachine.Name).Should(Equal(expectedMachine.Name)) 1673 } 1674 // Check UID 1675 if expectedMachine.UID != "" { 1676 g.Expect(actualMachine.UID).Should(Equal(expectedMachine.UID)) 1677 } 1678 // Check Namespace 1679 g.Expect(actualMachine.Namespace).Should(Equal(expectedMachine.Namespace)) 1680 // Check Labels 1681 for k, v := range expectedMachine.Labels { 1682 g.Expect(actualMachine.Labels).Should(HaveKeyWithValue(k, v)) 1683 } 1684 // Check Annotations 1685 for k, v := range expectedMachine.Annotations { 1686 g.Expect(actualMachine.Annotations).Should(HaveKeyWithValue(k, v)) 1687 } 1688 // Check Spec 1689 g.Expect(actualMachine.Spec).Should(BeComparableTo(expectedMachine.Spec)) 1690 // Check Finalizer 1691 if expectedMachine.Finalizers != nil { 1692 g.Expect(actualMachine.Finalizers).Should(Equal(expectedMachine.Finalizers)) 1693 } 1694 }