sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/controllers/upgrade_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  	"k8s.io/utils/ptr"
    31  	ctrl "sigs.k8s.io/controller-runtime"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  
    34  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    35  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal"
    37  	"sigs.k8s.io/cluster-api/internal/test/builder"
    38  	"sigs.k8s.io/cluster-api/internal/util/ssa"
    39  	"sigs.k8s.io/cluster-api/util"
    40  	"sigs.k8s.io/cluster-api/util/collections"
    41  )
    42  
    43  const UpdatedVersion string = "v1.17.4"
    44  const Host string = "nodomain.example.com"
    45  
    46  func TestKubeadmControlPlaneReconciler_RolloutStrategy_ScaleUp(t *testing.T) {
    47  	setup := func(t *testing.T, g *WithT) *corev1.Namespace {
    48  		t.Helper()
    49  
    50  		t.Log("Creating the namespace")
    51  		ns, err := env.CreateNamespace(ctx, "test-kcp-reconciler-rollout-scaleup")
    52  		g.Expect(err).ToNot(HaveOccurred())
    53  
    54  		return ns
    55  	}
    56  
    57  	teardown := func(t *testing.T, g *WithT, ns *corev1.Namespace) {
    58  		t.Helper()
    59  
    60  		t.Log("Deleting the namespace")
    61  		g.Expect(env.Delete(ctx, ns)).To(Succeed())
    62  	}
    63  
    64  	g := NewWithT(t)
    65  	namespace := setup(t, g)
    66  	defer teardown(t, g, namespace)
    67  
    68  	timeout := 30 * time.Second
    69  
    70  	cluster, kcp, genericInfrastructureMachineTemplate := createClusterWithControlPlane(namespace.Name)
    71  	g.Expect(env.Create(ctx, genericInfrastructureMachineTemplate, client.FieldOwner("manager"))).To(Succeed())
    72  	cluster.UID = types.UID(util.RandomString(10))
    73  	cluster.Spec.ControlPlaneEndpoint.Host = Host
    74  	cluster.Spec.ControlPlaneEndpoint.Port = 6443
    75  	cluster.Status.InfrastructureReady = true
    76  	kcp.UID = types.UID(util.RandomString(10))
    77  	kcp.Spec.KubeadmConfigSpec.ClusterConfiguration = nil
    78  	kcp.Spec.Replicas = ptr.To[int32](1)
    79  	setKCPHealthy(kcp)
    80  
    81  	r := &KubeadmControlPlaneReconciler{
    82  		Client:              env,
    83  		SecretCachingClient: secretCachingClient,
    84  		recorder:            record.NewFakeRecorder(32),
    85  		managementCluster: &fakeManagementCluster{
    86  			Management: &internal.Management{Client: env},
    87  			Workload: fakeWorkloadCluster{
    88  				Status: internal.ClusterStatus{Nodes: 1},
    89  			},
    90  		},
    91  		managementClusterUncached: &fakeManagementCluster{
    92  			Management: &internal.Management{Client: env},
    93  			Workload: fakeWorkloadCluster{
    94  				Status: internal.ClusterStatus{Nodes: 1},
    95  			},
    96  		},
    97  		ssaCache: ssa.NewCache(),
    98  	}
    99  	controlPlane := &internal.ControlPlane{
   100  		KCP:      kcp,
   101  		Cluster:  cluster,
   102  		Machines: nil,
   103  	}
   104  	controlPlane.InjectTestManagementCluster(r.managementCluster)
   105  
   106  	result, err := r.initializeControlPlane(ctx, controlPlane)
   107  	g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true}))
   108  	g.Expect(err).ToNot(HaveOccurred())
   109  
   110  	// initial setup
   111  	initialMachine := &clusterv1.MachineList{}
   112  	g.Eventually(func(g Gomega) {
   113  		// Nb. This Eventually block also forces the cache to update so that subsequent
   114  		// reconcile and upgradeControlPlane calls use the updated cache and avoids flakiness in the test.
   115  		g.Expect(env.List(ctx, initialMachine, client.InNamespace(cluster.Namespace))).To(Succeed())
   116  		g.Expect(initialMachine.Items).To(HaveLen(1))
   117  	}, timeout).Should(Succeed())
   118  	for i := range initialMachine.Items {
   119  		setMachineHealthy(&initialMachine.Items[i])
   120  	}
   121  
   122  	// change the KCP spec so the machine becomes outdated
   123  	kcp.Spec.Version = UpdatedVersion
   124  
   125  	// run upgrade the first time, expect we scale up
   126  	needingUpgrade := collections.FromMachineList(initialMachine)
   127  	controlPlane.Machines = needingUpgrade
   128  	result, err = r.upgradeControlPlane(ctx, controlPlane, needingUpgrade)
   129  	g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true}))
   130  	g.Expect(err).ToNot(HaveOccurred())
   131  	bothMachines := &clusterv1.MachineList{}
   132  	g.Eventually(func(g Gomega) {
   133  		g.Expect(env.List(ctx, bothMachines, client.InNamespace(cluster.Namespace))).To(Succeed())
   134  		g.Expect(bothMachines.Items).To(HaveLen(2))
   135  	}, timeout).Should(Succeed())
   136  
   137  	// run upgrade a second time, simulate that the node has not appeared yet but the machine exists
   138  
   139  	// Unhealthy control plane will be detected during reconcile loop and upgrade will never be called.
   140  	controlPlane = &internal.ControlPlane{
   141  		KCP:      kcp,
   142  		Cluster:  cluster,
   143  		Machines: collections.FromMachineList(bothMachines),
   144  	}
   145  	controlPlane.InjectTestManagementCluster(r.managementCluster)
   146  
   147  	result, err = r.reconcile(context.Background(), controlPlane)
   148  	g.Expect(err).ToNot(HaveOccurred())
   149  	g.Expect(result).To(BeComparableTo(ctrl.Result{RequeueAfter: preflightFailedRequeueAfter}))
   150  	g.Eventually(func(g Gomega) {
   151  		g.Expect(env.List(context.Background(), bothMachines, client.InNamespace(cluster.Namespace))).To(Succeed())
   152  		g.Expect(bothMachines.Items).To(HaveLen(2))
   153  	}, timeout).Should(Succeed())
   154  
   155  	// manually increase number of nodes, make control plane healthy again
   156  	r.managementCluster.(*fakeManagementCluster).Workload.Status.Nodes++
   157  	for i := range bothMachines.Items {
   158  		setMachineHealthy(&bothMachines.Items[i])
   159  	}
   160  	controlPlane.Machines = collections.FromMachineList(bothMachines)
   161  
   162  	machinesRequireUpgrade := collections.Machines{}
   163  	for i := range bothMachines.Items {
   164  		if bothMachines.Items[i].Spec.Version != nil && *bothMachines.Items[i].Spec.Version != UpdatedVersion {
   165  			machinesRequireUpgrade[bothMachines.Items[i].Name] = &bothMachines.Items[i]
   166  		}
   167  	}
   168  
   169  	// run upgrade the second time, expect we scale down
   170  	result, err = r.upgradeControlPlane(ctx, controlPlane, machinesRequireUpgrade)
   171  	g.Expect(err).ToNot(HaveOccurred())
   172  	g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true}))
   173  	finalMachine := &clusterv1.MachineList{}
   174  	g.Eventually(func(g Gomega) {
   175  		g.Expect(env.List(ctx, finalMachine, client.InNamespace(cluster.Namespace))).To(Succeed())
   176  		g.Expect(finalMachine.Items).To(HaveLen(1))
   177  		// assert that the deleted machine is the initial machine
   178  		g.Expect(finalMachine.Items[0].Name).ToNot(Equal(initialMachine.Items[0].Name))
   179  	}, timeout).Should(Succeed())
   180  }
   181  
   182  func TestKubeadmControlPlaneReconciler_RolloutStrategy_ScaleDown(t *testing.T) {
   183  	version := "v1.17.3"
   184  	g := NewWithT(t)
   185  
   186  	cluster, kcp, tmpl := createClusterWithControlPlane(metav1.NamespaceDefault)
   187  	cluster.Spec.ControlPlaneEndpoint.Host = "nodomain.example.com1"
   188  	cluster.Spec.ControlPlaneEndpoint.Port = 6443
   189  	kcp.Spec.Replicas = ptr.To[int32](3)
   190  	kcp.Spec.RolloutStrategy.RollingUpdate.MaxSurge.IntVal = 0
   191  	setKCPHealthy(kcp)
   192  
   193  	fmc := &fakeManagementCluster{
   194  		Machines: collections.Machines{},
   195  		Workload: fakeWorkloadCluster{
   196  			Status: internal.ClusterStatus{Nodes: 3},
   197  		},
   198  	}
   199  	objs := []client.Object{builder.GenericInfrastructureMachineTemplateCRD, cluster.DeepCopy(), kcp.DeepCopy(), tmpl.DeepCopy()}
   200  	for i := 0; i < 3; i++ {
   201  		name := fmt.Sprintf("test-%d", i)
   202  		m := &clusterv1.Machine{
   203  			ObjectMeta: metav1.ObjectMeta{
   204  				Namespace: cluster.Namespace,
   205  				Name:      name,
   206  				Labels:    internal.ControlPlaneMachineLabelsForCluster(kcp, cluster.Name),
   207  			},
   208  			Spec: clusterv1.MachineSpec{
   209  				Bootstrap: clusterv1.Bootstrap{
   210  					ConfigRef: &corev1.ObjectReference{
   211  						APIVersion: bootstrapv1.GroupVersion.String(),
   212  						Kind:       "KubeadmConfig",
   213  						Name:       name,
   214  					},
   215  				},
   216  				Version: &version,
   217  			},
   218  		}
   219  		cfg := &bootstrapv1.KubeadmConfig{
   220  			ObjectMeta: metav1.ObjectMeta{
   221  				Namespace: cluster.Namespace,
   222  				Name:      name,
   223  			},
   224  		}
   225  		objs = append(objs, m, cfg)
   226  		fmc.Machines.Insert(m)
   227  	}
   228  	fakeClient := newFakeClient(objs...)
   229  	fmc.Reader = fakeClient
   230  	r := &KubeadmControlPlaneReconciler{
   231  		Client:                    fakeClient,
   232  		SecretCachingClient:       fakeClient,
   233  		managementCluster:         fmc,
   234  		managementClusterUncached: fmc,
   235  	}
   236  
   237  	controlPlane := &internal.ControlPlane{
   238  		KCP:      kcp,
   239  		Cluster:  cluster,
   240  		Machines: nil,
   241  	}
   242  	controlPlane.InjectTestManagementCluster(r.managementCluster)
   243  
   244  	result, err := r.reconcile(ctx, controlPlane)
   245  	g.Expect(result).To(BeComparableTo(ctrl.Result{}))
   246  	g.Expect(err).ToNot(HaveOccurred())
   247  
   248  	machineList := &clusterv1.MachineList{}
   249  	g.Expect(fakeClient.List(ctx, machineList, client.InNamespace(cluster.Namespace))).To(Succeed())
   250  	g.Expect(machineList.Items).To(HaveLen(3))
   251  	for i := range machineList.Items {
   252  		setMachineHealthy(&machineList.Items[i])
   253  	}
   254  
   255  	// change the KCP spec so the machine becomes outdated
   256  	kcp.Spec.Version = UpdatedVersion
   257  
   258  	// run upgrade, expect we scale down
   259  	needingUpgrade := collections.FromMachineList(machineList)
   260  	controlPlane.Machines = needingUpgrade
   261  
   262  	result, err = r.upgradeControlPlane(ctx, controlPlane, needingUpgrade)
   263  	g.Expect(result).To(BeComparableTo(ctrl.Result{Requeue: true}))
   264  	g.Expect(err).ToNot(HaveOccurred())
   265  	remainingMachines := &clusterv1.MachineList{}
   266  	g.Expect(fakeClient.List(ctx, remainingMachines, client.InNamespace(cluster.Namespace))).To(Succeed())
   267  	g.Expect(remainingMachines.Items).To(HaveLen(2))
   268  }
   269  
   270  type machineOpt func(*clusterv1.Machine)
   271  
   272  func machine(name string, opts ...machineOpt) *clusterv1.Machine {
   273  	m := &clusterv1.Machine{
   274  		ObjectMeta: metav1.ObjectMeta{
   275  			Name:      name,
   276  			Namespace: metav1.NamespaceDefault,
   277  		},
   278  	}
   279  	for _, opt := range opts {
   280  		opt(m)
   281  	}
   282  	return m
   283  }