sigs.k8s.io/cluster-api@v1.6.3/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(HavePrefix(genericInfrastructureMachineTemplate.GetName()))
    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(HavePrefix(kcp.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)))
   372  	m2 := machine("machine-2", withFailureDomain("one"), withTimestamp(startDate.Add(-3*time.Hour)))
   373  	m3 := machine("machine-3", withFailureDomain("one"), withTimestamp(startDate.Add(-4*time.Hour)))
   374  	m4 := machine("machine-4", withFailureDomain("two"), withTimestamp(startDate.Add(-time.Hour)))
   375  	m5 := machine("machine-5", withFailureDomain("two"), withTimestamp(startDate.Add(-2*time.Hour)))
   376  	m6 := machine("machine-6", withFailureDomain("two"), withTimestamp(startDate.Add(-7*time.Hour)))
   377  	m7 := machine("machine-7", withFailureDomain("two"), withTimestamp(startDate.Add(-5*time.Hour)), withAnnotation("cluster.x-k8s.io/delete-machine"))
   378  	m8 := machine("machine-8", withFailureDomain("two"), withTimestamp(startDate.Add(-6*time.Hour)), withAnnotation("cluster.x-k8s.io/delete-machine"))
   379  
   380  	mc3 := collections.FromMachines(m1, m2, m3, m4, m5)
   381  	mc6 := collections.FromMachines(m6, m7, m8)
   382  	fd := clusterv1.FailureDomains{
   383  		"one": failureDomain(true),
   384  		"two": failureDomain(true),
   385  	}
   386  
   387  	needsUpgradeControlPlane := &internal.ControlPlane{
   388  		KCP:      &kcp,
   389  		Cluster:  &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}},
   390  		Machines: mc3,
   391  	}
   392  	upToDateControlPlane := &internal.ControlPlane{
   393  		KCP:     &kcp,
   394  		Cluster: &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}},
   395  		Machines: mc3.Filter(func(m *clusterv1.Machine) bool {
   396  			return m.Name != "machine-5"
   397  		}),
   398  	}
   399  	annotatedControlPlane := &internal.ControlPlane{
   400  		KCP:      &kcp,
   401  		Cluster:  &clusterv1.Cluster{Status: clusterv1.ClusterStatus{FailureDomains: fd}},
   402  		Machines: mc6,
   403  	}
   404  
   405  	testCases := []struct {
   406  		name             string
   407  		cp               *internal.ControlPlane
   408  		outDatedMachines collections.Machines
   409  		expectErr        bool
   410  		expectedMachine  clusterv1.Machine
   411  	}{
   412  		{
   413  			name:             "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade",
   414  			cp:               needsUpgradeControlPlane,
   415  			outDatedMachines: collections.FromMachines(m5),
   416  			expectErr:        false,
   417  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-5"}},
   418  		},
   419  		{
   420  			name:             "when there are no outdated machines, it returns the oldest machine in the largest failure domain",
   421  			cp:               upToDateControlPlane,
   422  			outDatedMachines: collections.New(),
   423  			expectErr:        false,
   424  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-3"}},
   425  		},
   426  		{
   427  			name:             "when there is a single machine marked with delete annotation key in machine collection, it returns only that marked machine",
   428  			cp:               annotatedControlPlane,
   429  			outDatedMachines: collections.FromMachines(m6, m7),
   430  			expectErr:        false,
   431  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}},
   432  		},
   433  		{
   434  			name:             "when there are machines marked with delete annotation key in machine collection, it returns the oldest marked machine first",
   435  			cp:               annotatedControlPlane,
   436  			outDatedMachines: collections.FromMachines(m7, m8),
   437  			expectErr:        false,
   438  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
   439  		},
   440  		{
   441  			name:             "when there are annotated machines which are part of the annotatedControlPlane but not in outdatedMachines, it returns the oldest marked machine first",
   442  			cp:               annotatedControlPlane,
   443  			outDatedMachines: collections.New(),
   444  			expectErr:        false,
   445  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
   446  		},
   447  		{
   448  			name:             "when there are machines needing upgrade, it returns the oldest machine in the failure domain with the most machines needing upgrade",
   449  			cp:               needsUpgradeControlPlane,
   450  			outDatedMachines: collections.FromMachines(m7, m3),
   451  			expectErr:        false,
   452  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-7"}},
   453  		},
   454  		{
   455  			name:             "when there is an up to date machine with delete annotation, while there are any outdated machines without annotatio that still exist, it returns oldest marked machine first",
   456  			cp:               upToDateControlPlane,
   457  			outDatedMachines: collections.FromMachines(m5, m3, m8, m7, m6, m1, m2),
   458  			expectErr:        false,
   459  			expectedMachine:  clusterv1.Machine{ObjectMeta: metav1.ObjectMeta{Name: "machine-8"}},
   460  		},
   461  	}
   462  
   463  	for _, tc := range testCases {
   464  		t.Run(tc.name, func(t *testing.T) {
   465  			g := NewWithT(t)
   466  
   467  			selectedMachine, err := selectMachineForScaleDown(tc.cp, tc.outDatedMachines)
   468  
   469  			if tc.expectErr {
   470  				g.Expect(err).To(HaveOccurred())
   471  				return
   472  			}
   473  
   474  			g.Expect(err).ToNot(HaveOccurred())
   475  			g.Expect(tc.expectedMachine.Name).To(Equal(selectedMachine.Name))
   476  		})
   477  	}
   478  }
   479  
   480  func TestPreflightChecks(t *testing.T) {
   481  	testCases := []struct {
   482  		name         string
   483  		kcp          *controlplanev1.KubeadmControlPlane
   484  		machines     []*clusterv1.Machine
   485  		expectResult ctrl.Result
   486  	}{
   487  		{
   488  			name:         "control plane without machines (not initialized) should pass",
   489  			kcp:          &controlplanev1.KubeadmControlPlane{},
   490  			expectResult: ctrl.Result{},
   491  		},
   492  		{
   493  			name: "control plane with a deleting machine should requeue",
   494  			kcp:  &controlplanev1.KubeadmControlPlane{},
   495  			machines: []*clusterv1.Machine{
   496  				{
   497  					ObjectMeta: metav1.ObjectMeta{
   498  						DeletionTimestamp: &metav1.Time{Time: time.Now()},
   499  					},
   500  				},
   501  			},
   502  			expectResult: ctrl.Result{RequeueAfter: deleteRequeueAfter},
   503  		},
   504  		{
   505  			name: "control plane without a nodeRef should requeue",
   506  			kcp:  &controlplanev1.KubeadmControlPlane{},
   507  			machines: []*clusterv1.Machine{
   508  				{
   509  					Status: clusterv1.MachineStatus{
   510  						NodeRef: nil,
   511  					},
   512  				},
   513  			},
   514  			expectResult: ctrl.Result{RequeueAfter: preflightFailedRequeueAfter},
   515  		},
   516  		{
   517  			name: "control plane with an unhealthy machine condition should requeue",
   518  			kcp:  &controlplanev1.KubeadmControlPlane{},
   519  			machines: []*clusterv1.Machine{
   520  				{
   521  					Status: clusterv1.MachineStatus{
   522  						NodeRef: &corev1.ObjectReference{
   523  							Kind: "Node",
   524  							Name: "node-1",
   525  						},
   526  						Conditions: clusterv1.Conditions{
   527  							*conditions.FalseCondition(controlplanev1.MachineAPIServerPodHealthyCondition, "fooReason", clusterv1.ConditionSeverityError, ""),
   528  							*conditions.TrueCondition(controlplanev1.MachineControllerManagerPodHealthyCondition),
   529  							*conditions.TrueCondition(controlplanev1.MachineSchedulerPodHealthyCondition),
   530  							*conditions.TrueCondition(controlplanev1.MachineEtcdPodHealthyCondition),
   531  							*conditions.TrueCondition(controlplanev1.MachineEtcdMemberHealthyCondition),
   532  						},
   533  					},
   534  				},
   535  			},
   536  			expectResult: ctrl.Result{RequeueAfter: preflightFailedRequeueAfter},
   537  		},
   538  		{
   539  			name: "control plane with an healthy machine and an healthy kcp condition should pass",
   540  			kcp: &controlplanev1.KubeadmControlPlane{
   541  				Status: controlplanev1.KubeadmControlPlaneStatus{
   542  					Conditions: clusterv1.Conditions{
   543  						*conditions.TrueCondition(controlplanev1.ControlPlaneComponentsHealthyCondition),
   544  						*conditions.TrueCondition(controlplanev1.EtcdClusterHealthyCondition),
   545  					},
   546  				},
   547  			},
   548  			machines: []*clusterv1.Machine{
   549  				{
   550  					Status: clusterv1.MachineStatus{
   551  						NodeRef: &corev1.ObjectReference{
   552  							Kind: "Node",
   553  							Name: "node-1",
   554  						},
   555  						Conditions: clusterv1.Conditions{
   556  							*conditions.TrueCondition(controlplanev1.MachineAPIServerPodHealthyCondition),
   557  							*conditions.TrueCondition(controlplanev1.MachineControllerManagerPodHealthyCondition),
   558  							*conditions.TrueCondition(controlplanev1.MachineSchedulerPodHealthyCondition),
   559  							*conditions.TrueCondition(controlplanev1.MachineEtcdPodHealthyCondition),
   560  							*conditions.TrueCondition(controlplanev1.MachineEtcdMemberHealthyCondition),
   561  						},
   562  					},
   563  				},
   564  			},
   565  			expectResult: ctrl.Result{},
   566  		},
   567  	}
   568  
   569  	for _, tt := range testCases {
   570  		t.Run(tt.name, func(t *testing.T) {
   571  			g := NewWithT(t)
   572  
   573  			r := &KubeadmControlPlaneReconciler{
   574  				recorder: record.NewFakeRecorder(32),
   575  			}
   576  			controlPlane := &internal.ControlPlane{
   577  				Cluster:  &clusterv1.Cluster{},
   578  				KCP:      tt.kcp,
   579  				Machines: collections.FromMachines(tt.machines...),
   580  			}
   581  			result, err := r.preflightChecks(context.TODO(), controlPlane)
   582  			g.Expect(err).ToNot(HaveOccurred())
   583  			g.Expect(result).To(BeComparableTo(tt.expectResult))
   584  		})
   585  	}
   586  }
   587  
   588  func TestPreflightCheckCondition(t *testing.T) {
   589  	condition := clusterv1.ConditionType("fooCondition")
   590  	testCases := []struct {
   591  		name      string
   592  		machine   *clusterv1.Machine
   593  		expectErr bool
   594  	}{
   595  		{
   596  			name:      "missing condition should return error",
   597  			machine:   &clusterv1.Machine{},
   598  			expectErr: true,
   599  		},
   600  		{
   601  			name: "false condition should return error",
   602  			machine: &clusterv1.Machine{
   603  				Status: clusterv1.MachineStatus{
   604  					Conditions: clusterv1.Conditions{
   605  						*conditions.FalseCondition(condition, "fooReason", clusterv1.ConditionSeverityError, ""),
   606  					},
   607  				},
   608  			},
   609  			expectErr: true,
   610  		},
   611  		{
   612  			name: "unknown condition should return error",
   613  			machine: &clusterv1.Machine{
   614  				Status: clusterv1.MachineStatus{
   615  					Conditions: clusterv1.Conditions{
   616  						*conditions.UnknownCondition(condition, "fooReason", ""),
   617  					},
   618  				},
   619  			},
   620  			expectErr: true,
   621  		},
   622  		{
   623  			name: "true condition should not return error",
   624  			machine: &clusterv1.Machine{
   625  				Status: clusterv1.MachineStatus{
   626  					Conditions: clusterv1.Conditions{
   627  						*conditions.TrueCondition(condition),
   628  					},
   629  				},
   630  			},
   631  			expectErr: false,
   632  		},
   633  	}
   634  
   635  	for _, tt := range testCases {
   636  		t.Run(tt.name, func(t *testing.T) {
   637  			g := NewWithT(t)
   638  
   639  			err := preflightCheckCondition("machine", tt.machine, condition)
   640  
   641  			if tt.expectErr {
   642  				g.Expect(err).To(HaveOccurred())
   643  				return
   644  			}
   645  			g.Expect(err).ToNot(HaveOccurred())
   646  		})
   647  	}
   648  }
   649  
   650  func failureDomain(controlPlane bool) clusterv1.FailureDomainSpec {
   651  	return clusterv1.FailureDomainSpec{
   652  		ControlPlane: controlPlane,
   653  	}
   654  }
   655  
   656  func withFailureDomain(fd string) machineOpt {
   657  	return func(m *clusterv1.Machine) {
   658  		m.Spec.FailureDomain = &fd
   659  	}
   660  }
   661  
   662  func withAnnotation(annotation string) machineOpt {
   663  	return func(m *clusterv1.Machine) {
   664  		m.ObjectMeta.Annotations = map[string]string{annotation: ""}
   665  	}
   666  }
   667  
   668  func withTimestamp(t time.Time) machineOpt {
   669  	return func(m *clusterv1.Machine) {
   670  		m.CreationTimestamp = metav1.NewTime(t)
   671  	}
   672  }