Skip to content

Commit 74e7bd2

Browse files
authored
Merge pull request #8698 from ykakarap/pr-double-rollout-e2e
🌱 add E2E test for MachineSet Preflight checks
2 parents 27195c0 + 9aafe17 commit 74e7bd2

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

test/e2e/cluster_upgrade_runtimesdk.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
4343
"sigs.k8s.io/cluster-api/util"
4444
"sigs.k8s.io/cluster-api/util/conditions"
45+
"sigs.k8s.io/cluster-api/util/patch"
4546
)
4647

4748
// The Cluster API test extension uses a ConfigMap named cluster-name + suffix to determine answers to the lifecycle hook calls;
@@ -221,6 +222,10 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
221222
input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"))
222223
},
223224
PreWaitForMachineDeploymentToBeUpgraded: func() {
225+
machineSetPreflightChecksTestHandler(ctx,
226+
input.BootstrapClusterProxy.GetClient(),
227+
clusterRef)
228+
224229
afterControlPlaneUpgradeTestHandler(ctx,
225230
input.BootstrapClusterProxy.GetClient(),
226231
clusterRef,
@@ -281,6 +286,104 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
281286
})
282287
}
283288

289+
// machineSetPreflightChecksTestHandler verifies the MachineSet preflight checks.
290+
// At this point in the test the ControlPlane is upgraded to the new version and the upgrade to the MachineDeployments
291+
// should be blocked by the AfterControlPlaneUpgrade hook.
292+
// Test the MachineSet preflight checks by scaling up the MachineDeployment. The creation on the new Machine
293+
// should be blocked because the preflight checks should not pass (kubeadm version skew preflight check should fail).
294+
func machineSetPreflightChecksTestHandler(ctx context.Context, c client.Client, clusterRef types.NamespacedName) {
295+
// Verify that the hook is called and the topology reconciliation is blocked.
296+
hookName := "AfterControlPlaneUpgrade"
297+
Eventually(func() error {
298+
if err := checkLifecycleHooksCalledAtLeastOnce(ctx, c, clusterRef, []string{hookName}); err != nil {
299+
return err
300+
}
301+
302+
cluster := framework.GetClusterByName(ctx, framework.GetClusterByNameInput{
303+
Name: clusterRef.Name, Namespace: clusterRef.Namespace, Getter: c})
304+
305+
if !clusterConditionShowsHookBlocking(cluster, hookName) {
306+
return errors.Errorf("Blocking condition for %s not found on Cluster object", hookName)
307+
}
308+
309+
return nil
310+
}, 30*time.Second).Should(Succeed(), "%s has not been called", hookName)
311+
312+
// Scale up the MachineDeployment
313+
machineDeployments := framework.GetMachineDeploymentsByCluster(ctx, framework.GetMachineDeploymentsByClusterInput{
314+
Lister: c,
315+
ClusterName: clusterRef.Name,
316+
Namespace: clusterRef.Namespace,
317+
})
318+
md := machineDeployments[0]
319+
320+
// Note: It is fair to assume that the Cluster is ClusterClass based since RuntimeSDK
321+
// is only supported for ClusterClass based Clusters.
322+
patchHelper, err := patch.NewHelper(md, c)
323+
Expect(err).To(BeNil())
324+
325+
// Scale up the MachineDeployment.
326+
// IMPORTANT: Since the MachineDeployment is pending an upgrade at this point the topology controller will not push any changes
327+
// to the MachineDeployment. Therefore, the changes made to the MachineDeployment here will not be replaced
328+
// until the AfterControlPlaneUpgrade hook unblocks the upgrade.
329+
*md.Spec.Replicas++
330+
Eventually(func() error {
331+
return patchHelper.Patch(ctx, md)
332+
}).Should(Succeed(), "Failed to scale up the MachineDeployment %s", klog.KObj(md))
333+
// Verify the MachineDeployment updated replicas are not overridden by the topology controller.
334+
// Note: This verifies that the topology controller in fact holds any reconciliation of this MachineDeployment.
335+
Consistently(func(g Gomega) {
336+
// Get the updated MachineDeployment.
337+
targetMD := &clusterv1.MachineDeployment{}
338+
// Wrap in an Eventually block for additional safety. Since all of this is in a Consistently block it
339+
// will fail if we hit a transient error like a network flake.
340+
g.Eventually(func() error {
341+
return c.Get(ctx, client.ObjectKeyFromObject(md), targetMD)
342+
}).Should(Succeed(), "Failed to get MachineDeployment %s", klog.KObj(md))
343+
// Verify replicas are not overridden.
344+
g.Expect(targetMD.Spec.Replicas).To(Equal(md.Spec.Replicas))
345+
}, 10*time.Second, 1*time.Second)
346+
347+
// Since the MachineDeployment is scaled up (overriding the topology controller) at this point the MachineSet would
348+
// also scale up. However, a new Machine creation would be blocked by one of the MachineSet preflight checks (KubeadmVersionSkew).
349+
// Verify the MachineSet is blocking new Machine creation.
350+
Eventually(func(g Gomega) {
351+
machineSets := framework.GetMachineSetsByDeployment(ctx, framework.GetMachineSetsByDeploymentInput{
352+
Lister: c,
353+
MDName: md.Name,
354+
Namespace: md.Namespace,
355+
})
356+
g.Expect(conditions.IsFalse(machineSets[0], clusterv1.MachinesCreatedCondition)).To(BeTrue())
357+
machinesCreatedCondition := conditions.Get(machineSets[0], clusterv1.MachinesCreatedCondition)
358+
g.Expect(machinesCreatedCondition).NotTo(BeNil())
359+
g.Expect(machinesCreatedCondition.Reason).To(Equal(clusterv1.PreflightCheckFailedReason))
360+
g.Expect(machineSets[0].Spec.Replicas).To(Equal(md.Spec.Replicas))
361+
}).Should(Succeed(), "New Machine creation not blocked by MachineSet preflight checks")
362+
363+
// Verify that the MachineSet is not creating the new Machine.
364+
// No new machines should be created for this MachineDeployment even though it is scaled up.
365+
// Creation of new Machines will be blocked by MachineSet preflight checks (KubeadmVersionSkew).
366+
Consistently(func(g Gomega) {
367+
originalReplicas := int(*md.Spec.Replicas - 1)
368+
machines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{
369+
Lister: c,
370+
ClusterName: clusterRef.Name,
371+
Namespace: clusterRef.Namespace,
372+
MachineDeployment: *md,
373+
})
374+
g.Expect(machines).To(HaveLen(originalReplicas), "New Machines should not be created")
375+
}, 10*time.Second, time.Second)
376+
377+
// Scale down the MachineDeployment to the original replicas to restore to the state of the MachineDeployment
378+
// it existed in before this test block.
379+
patchHelper, err = patch.NewHelper(md, c)
380+
Expect(err).To(BeNil())
381+
*md.Spec.Replicas--
382+
Eventually(func() error {
383+
return patchHelper.Patch(ctx, md)
384+
}).Should(Succeed(), "Failed to scale down the MachineDeployment %s", klog.KObj(md))
385+
}
386+
284387
// extensionConfig generates an ExtensionConfig.
285388
// We make sure this cluster-wide object does not conflict with others by using a random generated
286389
// name and a NamespaceSelector selecting on the namespace of the current test.

test/framework/machineset_helpers.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2023 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 framework
18+
19+
import (
20+
"context"
21+
22+
. "github.com/onsi/gomega"
23+
"k8s.io/klog/v2"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
26+
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
27+
)
28+
29+
// GetMachineSetsByDeploymentInput is the input for GetMachineSetsByDeployment.
30+
type GetMachineSetsByDeploymentInput struct {
31+
Lister Lister
32+
MDName string
33+
Namespace string
34+
}
35+
36+
// GetMachineSetsByDeployment returns the MachineSets objects for a MachineDeployment.
37+
// Important! this method relies on labels that are created by the CAPI controllers during the first reconciliation, so
38+
// it is necessary to ensure this is already happened before calling it.
39+
func GetMachineSetsByDeployment(ctx context.Context, input GetMachineSetsByDeploymentInput) []*clusterv1.MachineSet {
40+
machineSetList := &clusterv1.MachineSetList{}
41+
Eventually(func() error {
42+
return input.Lister.List(ctx, machineSetList, client.InNamespace(input.Namespace), client.MatchingLabels{clusterv1.MachineDeploymentNameLabel: input.MDName})
43+
}, retryableOperationTimeout, retryableOperationInterval).Should(Succeed(), "Failed to list MachineSets for MachineDeployment %s", klog.KRef(input.Namespace, input.MDName))
44+
45+
machineSets := make([]*clusterv1.MachineSet, len(machineSetList.Items))
46+
for i := range machineSetList.Items {
47+
machineSets[i] = &machineSetList.Items[i]
48+
}
49+
return machineSets
50+
}

0 commit comments

Comments
 (0)