sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/controllers/scale_test.go (about) 1 /* 2 Copyright 2020 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 controllers 18 19 import ( 20 "context" 21 "fmt" 22 "testing" 23 "time" 24 25 . "github.com/onsi/gomega" 26 corev1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/client-go/tools/record" 30 ctrl "sigs.k8s.io/controller-runtime" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 33 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 34 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 35 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 36 "sigs.k8s.io/cluster-api/controlplane/kubeadm/internal" 37 "sigs.k8s.io/cluster-api/util" 38 "sigs.k8s.io/cluster-api/util/collections" 39 "sigs.k8s.io/cluster-api/util/conditions" 40 ) 41 42 func TestKubeadmControlPlaneReconciler_initializeControlPlane(t *testing.T) { 43 setup := func(t *testing.T, g *WithT) *corev1.Namespace { 44 t.Helper() 45 46 t.Log("Creating the namespace") 47 ns, err := env.CreateNamespace(ctx, "test-kcp-reconciler-initializecontrolplane") 48 g.Expect(err).ToNot(HaveOccurred()) 49 50 return ns 51 } 52 53 teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace) { 54 t.Helper() 55 56 t.Log("Deleting the namespace") 57 g.Expect(env.Delete(ctx, ns)).To(Succeed()) 58 } 59 60 g := NewWithT(t) 61 namespace := setup(t, g) 62 defer teardown(t, g, namespace) 63 64 cluster, kcp, genericInfrastructureMachineTemplate := createClusterWithControlPlane(namespace.Name) 65 g.Expect(env.Create(ctx, genericInfrastructureMachineTemplate, client.FieldOwner("manager"))).To(Succeed()) 66 kcp.UID = types.UID(util.RandomString(10)) 67 68 r := &KubeadmControlPlaneReconciler{ 69 Client: env, 70 recorder: record.NewFakeRecorder(32), 71 managementClusterUncached: &fakeManagementCluster{ 72 Management: &internal.Management{Client: env}, 73 Workload: fakeWorkloadCluster{}, 74 }, 75 } 76 controlPlane := &internal.ControlPlane{ 77 Cluster: cluster, 78 KCP: kcp, 79 } 80 81 result, err := r.initializeControlPlane(ctx, controlPlane) 82 g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true})) 83 g.Expect(err).ToNot(HaveOccurred()) 84 85 machineList := &clusterv1.MachineList{} 86 g.Expect(env.GetAPIReader().List(ctx, machineList, client.InNamespace(cluster.Namespace))).To(Succeed()) 87 g.Expect(machineList.Items).To(HaveLen(1)) 88 89 res, err := collections.GetFilteredMachinesForCluster(ctx, env.GetAPIReader(), cluster, collections.OwnedMachines(kcp)) 90 g.Expect(res).To(HaveLen(1)) 91 g.Expect(err).ToNot(HaveOccurred()) 92 93 g.Expect(machineList.Items[0].Namespace).To(Equal(cluster.Namespace)) 94 g.Expect(machineList.Items[0].Name).To(HavePrefix(kcp.Name)) 95 96 g.Expect(machineList.Items[0].Spec.InfrastructureRef.Namespace).To(Equal(cluster.Namespace)) 97 g.Expect(machineList.Items[0].Spec.InfrastructureRef.Name).To(Equal(machineList.Items[0].Name)) 98 g.Expect(machineList.Items[0].Spec.InfrastructureRef.APIVersion).To(Equal(genericInfrastructureMachineTemplate.GetAPIVersion())) 99 g.Expect(machineList.Items[0].Spec.InfrastructureRef.Kind).To(Equal("GenericInfrastructureMachine")) 100 101 g.Expect(machineList.Items[0].Spec.Bootstrap.ConfigRef.Namespace).To(Equal(cluster.Namespace)) 102 g.Expect(machineList.Items[0].Spec.Bootstrap.ConfigRef.Name).To(Equal(machineList.Items[0].Name)) 103 g.Expect(machineList.Items[0].Spec.Bootstrap.ConfigRef.APIVersion).To(Equal(bootstrapv1.GroupVersion.String())) 104 g.Expect(machineList.Items[0].Spec.Bootstrap.ConfigRef.Kind).To(Equal("KubeadmConfig")) 105 } 106 107 func TestKubeadmControlPlaneReconciler_scaleUpControlPlane(t *testing.T) { 108 t.Run("creates a control plane Machine if preflight checks pass", func(t *testing.T) { 109 setup := func(t *testing.T, g *WithT) *corev1.Namespace { 110 t.Helper() 111 112 t.Log("Creating the namespace") 113 ns, err := env.CreateNamespace(ctx, "test-kcp-reconciler-scaleupcontrolplane") 114 g.Expect(err).ToNot(HaveOccurred()) 115 116 return ns 117 } 118 119 teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace) { 120 t.Helper() 121 122 t.Log("Deleting the namespace") 123 g.Expect(env.Delete(ctx, ns)).To(Succeed()) 124 } 125 126 g := NewWithT(t) 127 namespace := setup(t, g) 128 defer teardown(t, g, namespace) 129 130 cluster, kcp, genericInfrastructureMachineTemplate := createClusterWithControlPlane(namespace.Name) 131 g.Expect(env.Create(ctx, genericInfrastructureMachineTemplate, client.FieldOwner("manager"))).To(Succeed()) 132 kcp.UID = types.UID(util.RandomString(10)) 133 setKCPHealthy(kcp) 134 135 fmc := &fakeManagementCluster{ 136 Machines: collections.New(), 137 Workload: fakeWorkloadCluster{}, 138 } 139 140 for i := 0; i < 2; i++ { 141 m, _ := createMachineNodePair(fmt.Sprintf("test-%d", i), cluster, kcp, true) 142 setMachineHealthy(m) 143 fmc.Machines.Insert(m) 144 } 145 146 r := &KubeadmControlPlaneReconciler{ 147 Client: env, 148 managementCluster: fmc, 149 managementClusterUncached: fmc, 150 recorder: record.NewFakeRecorder(32), 151 } 152 controlPlane := &internal.ControlPlane{ 153 KCP: kcp, 154 Cluster: cluster, 155 Machines: fmc.Machines, 156 } 157 158 result, err := r.scaleUpControlPlane(ctx, controlPlane) 159 g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true})) 160 g.Expect(err).ToNot(HaveOccurred()) 161 162 controlPlaneMachines := clusterv1.MachineList{} 163 g.Expect(env.GetAPIReader().List(ctx, &controlPlaneMachines, client.InNamespace(namespace.Name))).To(Succeed()) 164 // A new machine should have been created. 165 // Note: expected length is 1 because only the newly created machine is on API server. Other machines are 166 // in-memory only during the test. 167 g.Expect(controlPlaneMachines.Items).To(HaveLen(1)) 168 }) 169 t.Run("does not create a control plane Machine if preflight checks fail", func(t *testing.T) { 170 setup := func(t *testing.T, g *WithT) *corev1.Namespace { 171 t.Helper() 172 173 t.Log("Creating the namespace") 174 ns, err := env.CreateNamespace(ctx, "test-kcp-reconciler-scaleupcontrolplane") 175 g.Expect(err).ToNot(HaveOccurred()) 176 177 return ns 178 } 179 180 teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace) { 181 t.Helper() 182 183 t.Log("Deleting the namespace") 184 g.Expect(env.Delete(ctx, ns)).To(Succeed()) 185 } 186 187 g := NewWithT(t) 188 namespace := setup(t, g) 189 defer teardown(t, g, namespace) 190 191 cluster, kcp, genericInfrastructureMachineTemplate := createClusterWithControlPlane(namespace.Name) 192 g.Expect(env.Create(ctx, genericInfrastructureMachineTemplate, client.FieldOwner("manager"))).To(Succeed()) 193 kcp.UID = types.UID(util.RandomString(10)) 194 cluster.UID = types.UID(util.RandomString(10)) 195 cluster.Spec.ControlPlaneEndpoint.Host = "nodomain.example.com" 196 cluster.Spec.ControlPlaneEndpoint.Port = 6443 197 cluster.Status.InfrastructureReady = true 198 199 beforeMachines := collections.New() 200 for i := 0; i < 2; i++ { 201 m, _ := createMachineNodePair(fmt.Sprintf("test-%d", i), cluster.DeepCopy(), kcp.DeepCopy(), true) 202 beforeMachines.Insert(m) 203 } 204 205 fmc := &fakeManagementCluster{ 206 Machines: beforeMachines.DeepCopy(), 207 Workload: fakeWorkloadCluster{}, 208 } 209 210 r := &KubeadmControlPlaneReconciler{ 211 Client: env, 212 SecretCachingClient: secretCachingClient, 213 managementCluster: fmc, 214 managementClusterUncached: fmc, 215 recorder: record.NewFakeRecorder(32), 216 } 217 218 controlPlane, adoptableMachineFound, err := r.initControlPlaneScope(ctx, cluster, kcp) 219 g.Expect(err).ToNot(HaveOccurred()) 220 g.Expect(adoptableMachineFound).To(BeFalse()) 221 222 result, err := r.scaleUpControlPlane(context.Background(), controlPlane) 223 g.Expect(err).ToNot(HaveOccurred()) 224 g.Expect(result).To(BeComparableTo(ctrl.Result{RequeueAfter: preflightFailedRequeueAfter})) 225 226 // scaleUpControlPlane is never called due to health check failure and new machine is not created to scale up. 227 controlPlaneMachines := &clusterv1.MachineList{} 228 g.Expect(env.GetAPIReader().List(context.Background(), controlPlaneMachines, client.InNamespace(namespace.Name))).To(Succeed()) 229 // No new machine should be created. 230 // Note: expected length is 0 because no machine is created and hence no machine is on the API server. 231 // Other machines are in-memory only during the test. 232 g.Expect(controlPlaneMachines.Items).To(BeEmpty()) 233 234 endMachines := collections.FromMachineList(controlPlaneMachines) 235 for _, m := range endMachines { 236 bm, ok := beforeMachines[m.Name] 237 g.Expect(ok).To(BeTrue()) 238 g.Expect(m).To(BeComparableTo(bm)) 239 } 240 }) 241 } 242 243 func TestKubeadmControlPlaneReconciler_scaleDownControlPlane_NoError(t *testing.T) { 244 t.Run("deletes control plane Machine if preflight checks pass", func(t *testing.T) { 245 g := NewWithT(t) 246 247 machines := map[string]*clusterv1.Machine{ 248 "one": machine("one"), 249 } 250 setMachineHealthy(machines["one"]) 251 fakeClient := newFakeClient(machines["one"]) 252 253 r := &KubeadmControlPlaneReconciler{ 254 recorder: record.NewFakeRecorder(32), 255 Client: fakeClient, 256 SecretCachingClient: fakeClient, 257 managementCluster: &fakeManagementCluster{ 258 Workload: fakeWorkloadCluster{}, 259 }, 260 } 261 262 cluster := &clusterv1.Cluster{} 263 kcp := &controlplanev1.KubeadmControlPlane{ 264 Spec: controlplanev1.KubeadmControlPlaneSpec{ 265 Version: "v1.19.1", 266 }, 267 } 268 setKCPHealthy(kcp) 269 controlPlane := &internal.ControlPlane{ 270 KCP: kcp, 271 Cluster: cluster, 272 Machines: machines, 273 } 274 controlPlane.InjectTestManagementCluster(r.managementCluster) 275 276 result, err := r.scaleDownControlPlane(context.Background(), controlPlane, controlPlane.Machines) 277 g.Expect(err).ToNot(HaveOccurred()) 278 g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true})) 279 280 controlPlaneMachines := clusterv1.MachineList{} 281 g.Expect(fakeClient.List(context.Background(), &controlPlaneMachines)).To(Succeed()) 282 g.Expect(controlPlaneMachines.Items).To(BeEmpty()) 283 }) 284 t.Run("deletes the oldest control plane Machine even if preflight checks fails", func(t *testing.T) { 285 g := NewWithT(t) 286 287 machines := map[string]*clusterv1.Machine{ 288 "one": machine("one", withTimestamp(time.Now().Add(-1*time.Minute))), 289 "two": machine("two", withTimestamp(time.Now())), 290 "three": machine("three", withTimestamp(time.Now())), 291 } 292 setMachineHealthy(machines["two"]) 293 setMachineHealthy(machines["three"]) 294 fakeClient := newFakeClient(machines["one"], machines["two"], machines["three"]) 295 296 r := &KubeadmControlPlaneReconciler{ 297 recorder: record.NewFakeRecorder(32), 298 Client: fakeClient, 299 SecretCachingClient: fakeClient, 300 managementCluster: &fakeManagementCluster{ 301 Workload: fakeWorkloadCluster{}, 302 }, 303 } 304 305 cluster := &clusterv1.Cluster{} 306 kcp := &controlplanev1.KubeadmControlPlane{ 307 Spec: controlplanev1.KubeadmControlPlaneSpec{ 308 Version: "v1.19.1", 309 }, 310 } 311 controlPlane := &internal.ControlPlane{ 312 KCP: kcp, 313 Cluster: cluster, 314 Machines: machines, 315 } 316 controlPlane.InjectTestManagementCluster(r.managementCluster) 317 318 result, err := r.scaleDownControlPlane(context.Background(), controlPlane, controlPlane.Machines) 319 g.Expect(err).ToNot(HaveOccurred()) 320 g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true})) 321 322 controlPlaneMachines := clusterv1.MachineList{} 323 g.Expect(fakeClient.List(context.Background(), &controlPlaneMachines)).To(Succeed()) 324 g.Expect(controlPlaneMachines.Items).To(HaveLen(2)) 325 }) 326 327 t.Run("does not scale down if preflight checks fail on any machine other than the one being deleted", func(t *testing.T) { 328 g := NewWithT(t) 329 330 machines := map[string]*clusterv1.Machine{ 331 "one": machine("one", withTimestamp(time.Now().Add(-1*time.Minute))), 332 "two": machine("two", withTimestamp(time.Now())), 333 "three": machine("three", withTimestamp(time.Now())), 334 } 335 setMachineHealthy(machines["three"]) 336 fakeClient := newFakeClient(machines["one"], machines["two"], machines["three"]) 337 338 r := &KubeadmControlPlaneReconciler{ 339 recorder: record.NewFakeRecorder(32), 340 Client: fakeClient, 341 SecretCachingClient: fakeClient, 342 managementCluster: &fakeManagementCluster{ 343 Workload: fakeWorkloadCluster{}, 344 }, 345 } 346 347 cluster := &clusterv1.Cluster{} 348 kcp := &controlplanev1.KubeadmControlPlane{} 349 controlPlane := &internal.ControlPlane{ 350 KCP: kcp, 351 Cluster: cluster, 352 Machines: machines, 353 } 354 controlPlane.InjectTestManagementCluster(r.managementCluster) 355 356 result, err := r.scaleDownControlPlane(context.Background(), controlPlane, controlPlane.Machines) 357 g.Expect(err).ToNot(HaveOccurred()) 358 g.Expect(result).To(BeComparableTo(ctrl.Result{RequeueAfter: preflightFailedRequeueAfter})) 359 360 controlPlaneMachines := clusterv1.MachineList{} 361 g.Expect(fakeClient.List(context.Background(), &controlPlaneMachines)).To(Succeed()) 362 g.Expect(controlPlaneMachines.Items).To(HaveLen(3)) 363 }) 364 } 365 366 func TestSelectMachineForScaleDown(t *testing.T) { 367 kcp := controlplanev1.KubeadmControlPlane{ 368 Spec: controlplanev1.KubeadmControlPlaneSpec{}, 369 } 370 startDate := time.Date(2000, 1, 1, 1, 0, 0, 0, time.UTC) 371 m1 := machine("machine-1", withFailureDomain("one"), withTimestamp(startDate.Add(time.Hour)), machineOpt(withNodeRef("machine-1"))) 372 m2 := machine("machine-2", withFailureDomain("one"), withTimestamp(startDate.Add(-3*time.Hour)), machineOpt(withNodeRef("machine-2"))) 373 m3 := machine("machine-3", withFailureDomain("one"), withTimestamp(startDate.Add(-4*time.Hour)), machineOpt(withNodeRef("machine-3"))) 374 m4 := machine("machine-4", withFailureDomain("two"), withTimestamp(startDate.Add(-time.Hour)), machineOpt(withNodeRef("machine-4"))) 375 m5 := machine("machine-5", withFailureDomain("two"), withTimestamp(startDate.Add(-2*time.Hour)), machineOpt(withNodeRef("machine-5"))) 376 m6 := machine("machine-6", withFailureDomain("two"), withTimestamp(startDate.Add(-7*time.Hour)), machineOpt(withNodeRef("machine-6"))) 377 m7 := machine("machine-7", withFailureDomain("two"), withTimestamp(startDate.Add(-5*time.Hour)), 378 withAnnotation("cluster.x-k8s.io/delete-machine"), machineOpt(withNodeRef("machine-7"))) 379 m8 := machine("machine-8", withFailureDomain("two"), withTimestamp(startDate.Add(-6*time.Hour)), 380 withAnnotation("cluster.x-k8s.io/delete-machine"), machineOpt(withNodeRef("machine-8"))) 381 m9 := machine("machine-9", withFailureDomain("two"), withTimestamp(startDate.Add(-5*time.Hour)), 382 machineOpt(withNodeRef("machine-9"))) 383 m10 := machine("machine-10", withFailureDomain("two"), withTimestamp(startDate.Add(-4*time.Hour)), 384 machineOpt(withNodeRef("machine-10")), machineOpt(withUnhealthyAPIServerPod())) 385 m11 := machine("machine-11", withFailureDomain("two"), withTimestamp(startDate.Add(-3*time.Hour)), 386 machineOpt(withNodeRef("machine-11")), machineOpt(withUnhealthyEtcdMember())) 387 388 mc3 := collections.FromMachines(m1, m2, m3, m4, m5) 389 mc6 := collections.FromMachines(m6, m7, m8) 390 mc9 := collections.FromMachines(m9, m10, m11) 391 fd := clusterv1.FailureDomains{ 392 "one": failureDomain(true), 393 "two": failureDomain(true), 394 } 395 396 needsUpgradeControlPlane := &internal.ControlPlane{ 397 KCP: &kcp, 398 Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}}, 399 Machines: mc3, 400 } 401 needsUpgradeControlPlane1 := &internal.ControlPlane{ 402 KCP: &kcp, 403 Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}}, 404 Machines: mc9, 405 } 406 upToDateControlPlane := &internal.ControlPlane{ 407 KCP: &kcp, 408 Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}}, 409 Machines: mc3.Filter(func(m *clusterv1.Machine) bool { 410 return m.Name != "machine-5" 411 }), 412 } 413 annotatedControlPlane := &internal.ControlPlane{ 414 KCP: &kcp, 415 Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}}, 416 Machines: mc6, 417 } 418 419 testCases := []struct { 420 name string 421 cp *internal.ControlPlane 422 outDatedMachines collections.Machines 423 expectErr bool 424 expectedMachine clusterv1.Machine 425 }{ 426 { 427 name: "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade", 428 cp: needsUpgradeControlPlane, 429 outDatedMachines: collections.FromMachines(m5), 430 expectErr: false, 431 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-5"}}, 432 }, 433 { 434 name: "when there are no outdated machines, it returns the oldest machine in the largest failure domain", 435 cp: upToDateControlPlane, 436 outDatedMachines: collections.New(), 437 expectErr: false, 438 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}}, 439 }, 440 { 441 name: "when there is a single machine marked with delete annotation key in machine collection, it returns only that marked machine", 442 cp: annotatedControlPlane, 443 outDatedMachines: collections.FromMachines(m6, m7), 444 expectErr: false, 445 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}}, 446 }, 447 { 448 name: "when there are machines marked with delete annotation key in machine collection, it returns the oldest marked machine first", 449 cp: annotatedControlPlane, 450 outDatedMachines: collections.FromMachines(m7, m8), 451 expectErr: false, 452 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}}, 453 }, 454 { 455 name: "when there are annotated machines which are part of the annotatedControlPlane but not in outdatedMachines, it returns the oldest marked machine first", 456 cp: annotatedControlPlane, 457 outDatedMachines: collections.New(), 458 expectErr: false, 459 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}}, 460 }, 461 { 462 name: "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade", 463 cp: needsUpgradeControlPlane, 464 outDatedMachines: collections.FromMachines(m7, m3), 465 expectErr: false, 466 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}}, 467 }, 468 { 469 name: "when there is an up to date machine with delete annotation, while there are any outdated machines without annotation that still exist, it returns oldest marked machine first", 470 cp: upToDateControlPlane, 471 outDatedMachines: collections.FromMachines(m5, m3, m8, m7, m6, m1, m2), 472 expectErr: false, 473 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}}, 474 }, 475 { 476 name: "when there are machines needing upgrade, it returns the single unhealthy machine with MachineAPIServerPodHealthyCondition set to False", 477 cp: needsUpgradeControlPlane1, 478 outDatedMachines: collections.FromMachines(m9, m10), 479 expectErr: false, 480 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-10"}}, 481 }, 482 { 483 name: "when there are machines needing upgrade, it returns the oldest unhealthy machine with MachineEtcdMemberHealthyCondition set to False", 484 cp: needsUpgradeControlPlane1, 485 outDatedMachines: collections.FromMachines(m9, m10, m11), 486 expectErr: false, 487 expectedMachine: clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-10"}}, 488 }, 489 } 490 491 for _, tc := range testCases { 492 t.Run(tc.name, func(t *testing.T) { 493 g := NewWithT(t) 494 495 selectedMachine, err := selectMachineForScaleDown(ctx, tc.cp, tc.outDatedMachines) 496 497 if tc.expectErr { 498 g.Expect(err).To(HaveOccurred()) 499 return 500 } 501 502 g.Expect(err).ToNot(HaveOccurred()) 503 g.Expect(tc.expectedMachine.Name).To(Equal(selectedMachine.Name)) 504 }) 505 } 506 } 507 508 func TestPreflightChecks(t *testing.T) { 509 testCases := []struct { 510 name string 511 kcp *controlplanev1.KubeadmControlPlane 512 machines []*clusterv1.Machine 513 expectResult ctrl.Result 514 }{ 515 { 516 name: "control plane without machines (not initialized) should pass", 517 kcp: &controlplanev1.KubeadmControlPlane{}, 518 expectResult: ctrl.Result{}, 519 }, 520 { 521 name: "control plane with a deleting machine should requeue", 522 kcp: &controlplanev1.KubeadmControlPlane{}, 523 machines: []*clusterv1.Machine{ 524 { 525 ObjectMeta: metav1.ObjectMeta{ 526 DeletionTimestamp: &metav1.Time{Time: time.Now()}, 527 }, 528 }, 529 }, 530 expectResult: ctrl.Result{RequeueAfter: deleteRequeueAfter}, 531 }, 532 { 533 name: "control plane without a nodeRef should requeue", 534 kcp: &controlplanev1.KubeadmControlPlane{}, 535 machines: []*clusterv1.Machine{ 536 { 537 Status: clusterv1.MachineStatus{ 538 NodeRef: nil, 539 }, 540 }, 541 }, 542 expectResult: ctrl.Result{RequeueAfter: preflightFailedRequeueAfter}, 543 }, 544 { 545 name: "control plane with an unhealthy machine condition should requeue", 546 kcp: &controlplanev1.KubeadmControlPlane{}, 547 machines: []*clusterv1.Machine{ 548 { 549 Status: clusterv1.MachineStatus{ 550 NodeRef: &corev1.ObjectReference{ 551 Kind: "Node", 552 Name: "node-1", 553 }, 554 Conditions: clusterv1.Conditions{ 555 *conditions.FalseCondition(controlplanev1.MachineAPIServerPodHealthyCondition, "fooReason", clusterv1.ConditionSeverityError, ""), 556 *conditions.TrueCondition(controlplanev1.MachineControllerManagerPodHealthyCondition), 557 *conditions.TrueCondition(controlplanev1.MachineSchedulerPodHealthyCondition), 558 *conditions.TrueCondition(controlplanev1.MachineEtcdPodHealthyCondition), 559 *conditions.TrueCondition(controlplanev1.MachineEtcdMemberHealthyCondition), 560 }, 561 }, 562 }, 563 }, 564 expectResult: ctrl.Result{RequeueAfter: preflightFailedRequeueAfter}, 565 }, 566 { 567 name: "control plane with an healthy machine and an healthy kcp condition should pass", 568 kcp: &controlplanev1.KubeadmControlPlane{ 569 Status: controlplanev1.KubeadmControlPlaneStatus{ 570 Conditions: clusterv1.Conditions{ 571 *conditions.TrueCondition(controlplanev1.ControlPlaneComponentsHealthyCondition), 572 *conditions.TrueCondition(controlplanev1.EtcdClusterHealthyCondition), 573 }, 574 }, 575 }, 576 machines: []*clusterv1.Machine{ 577 { 578 Status: clusterv1.MachineStatus{ 579 NodeRef: &corev1.ObjectReference{ 580 Kind: "Node", 581 Name: "node-1", 582 }, 583 Conditions: clusterv1.Conditions{ 584 *conditions.TrueCondition(controlplanev1.MachineAPIServerPodHealthyCondition), 585 *conditions.TrueCondition(controlplanev1.MachineControllerManagerPodHealthyCondition), 586 *conditions.TrueCondition(controlplanev1.MachineSchedulerPodHealthyCondition), 587 *conditions.TrueCondition(controlplanev1.MachineEtcdPodHealthyCondition), 588 *conditions.TrueCondition(controlplanev1.MachineEtcdMemberHealthyCondition), 589 }, 590 }, 591 }, 592 }, 593 expectResult: ctrl.Result{}, 594 }, 595 } 596 597 for _, tt := range testCases { 598 t.Run(tt.name, func(t *testing.T) { 599 g := NewWithT(t) 600 601 r := &KubeadmControlPlaneReconciler{ 602 recorder: record.NewFakeRecorder(32), 603 } 604 controlPlane := &internal.ControlPlane{ 605 Cluster: &clusterv1.Cluster{}, 606 KCP: tt.kcp, 607 Machines: collections.FromMachines(tt.machines...), 608 } 609 result, err := r.preflightChecks(context.TODO(), controlPlane) 610 g.Expect(err).ToNot(HaveOccurred()) 611 g.Expect(result).To(BeComparableTo(tt.expectResult)) 612 }) 613 } 614 } 615 616 func TestPreflightCheckCondition(t *testing.T) { 617 condition := clusterv1.ConditionType("fooCondition") 618 testCases := []struct { 619 name string 620 machine *clusterv1.Machine 621 expectErr bool 622 }{ 623 { 624 name: "missing condition should return error", 625 machine: &clusterv1.Machine{}, 626 expectErr: true, 627 }, 628 { 629 name: "false condition should return error", 630 machine: &clusterv1.Machine{ 631 Status: clusterv1.MachineStatus{ 632 Conditions: clusterv1.Conditions{ 633 *conditions.FalseCondition(condition, "fooReason", clusterv1.ConditionSeverityError, ""), 634 }, 635 }, 636 }, 637 expectErr: true, 638 }, 639 { 640 name: "unknown condition should return error", 641 machine: &clusterv1.Machine{ 642 Status: clusterv1.MachineStatus{ 643 Conditions: clusterv1.Conditions{ 644 *conditions.UnknownCondition(condition, "fooReason", ""), 645 }, 646 }, 647 }, 648 expectErr: true, 649 }, 650 { 651 name: "true condition should not return error", 652 machine: &clusterv1.Machine{ 653 Status: clusterv1.MachineStatus{ 654 Conditions: clusterv1.Conditions{ 655 *conditions.TrueCondition(condition), 656 }, 657 }, 658 }, 659 expectErr: false, 660 }, 661 } 662 663 for _, tt := range testCases { 664 t.Run(tt.name, func(t *testing.T) { 665 g := NewWithT(t) 666 667 err := preflightCheckCondition("machine", tt.machine, condition) 668 669 if tt.expectErr { 670 g.Expect(err).To(HaveOccurred()) 671 return 672 } 673 g.Expect(err).ToNot(HaveOccurred()) 674 }) 675 } 676 } 677 678 func failureDomain(controlPlane bool) clusterv1.FailureDomainSpec { 679 return clusterv1.FailureDomainSpec{ 680 ControlPlane: controlPlane, 681 } 682 } 683 684 func withFailureDomain(fd string) machineOpt { 685 return func(m *clusterv1.Machine) { 686 m.Spec.FailureDomain = &fd 687 } 688 } 689 690 func withAnnotation(annotation string) machineOpt { 691 return func(m *clusterv1.Machine) { 692 m.ObjectMeta.Annotations = map[string]string{annotation: ""} 693 } 694 } 695 696 func withTimestamp(t time.Time) machineOpt { 697 return func(m *clusterv1.Machine) { 698 m.CreationTimestamp = metav1.NewTime(t) 699 } 700 }