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  }