Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 add E2E test for MachineSet Preflight checks #8698

Merged
merged 1 commit into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions test/e2e/cluster_upgrade_runtimesdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/conditions"
"sigs.k8s.io/cluster-api/util/patch"
)

// The Cluster API test extension uses a ConfigMap named cluster-name + suffix to determine answers to the lifecycle hook calls;
Expand Down Expand Up @@ -221,6 +222,10 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
input.E2EConfig.GetIntervals(specName, "wait-machine-upgrade"))
},
PreWaitForMachineDeploymentToBeUpgraded: func() {
machineSetPreflightChecksTestHandler(ctx,
input.BootstrapClusterProxy.GetClient(),
clusterRef)

afterControlPlaneUpgradeTestHandler(ctx,
input.BootstrapClusterProxy.GetClient(),
clusterRef,
Expand Down Expand Up @@ -281,6 +286,104 @@ func clusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() cl
})
}

// machineSetPreflightChecksTestHandler verifies the MachineSet preflight checks.
// At this point in the test the ControlPlane is upgraded to the new version and the upgrade to the MachineDeployments
// should be blocked by the AfterControlPlaneUpgrade hook.
// Test the MachineSet preflight checks by scaling up the MachineDeployment. The creation on the new Machine
// should be blocked because the preflight checks should not pass (kubeadm version skew preflight check should fail).
func machineSetPreflightChecksTestHandler(ctx context.Context, c client.Client, clusterRef types.NamespacedName) {
// Verify that the hook is called and the topology reconciliation is blocked.
hookName := "AfterControlPlaneUpgrade"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost wonder if this check for the AfterControlPlaneUpgrade hook should be generalized as part of every control plane upgrade flow, and not incorporated into this MachineDeployment validation. Is that a good idea, and viable?

(I'm assuming that this hook is always called after a control plane upgrade, as its name suggests.)

Copy link
Member

@sbueringer sbueringer Jun 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is only called if there is a Runtime Extension deployed. This test is the only one who uses Runtime Extensions.

To be honest, I"m not sure I understand what you mean with "generalized as part of every control plane upgrade flow"

Eventually(func() error {
if err := checkLifecycleHooksCalledAtLeastOnce(ctx, c, clusterRef, []string{hookName}); err != nil {
return err
}

cluster := framework.GetClusterByName(ctx, framework.GetClusterByNameInput{
Name: clusterRef.Name, Namespace: clusterRef.Namespace, Getter: c})

if !clusterConditionShowsHookBlocking(cluster, hookName) {
return errors.Errorf("Blocking condition for %s not found on Cluster object", hookName)
}

return nil
}, 30*time.Second).Should(Succeed(), "%s has not been called", hookName)

// Scale up the MachineDeployment
machineDeployments := framework.GetMachineDeploymentsByCluster(ctx, framework.GetMachineDeploymentsByClusterInput{
Lister: c,
ClusterName: clusterRef.Name,
Namespace: clusterRef.Namespace,
})
md := machineDeployments[0]

// Note: It is fair to assume that the Cluster is ClusterClass based since RuntimeSDK
// is only supported for ClusterClass based Clusters.
patchHelper, err := patch.NewHelper(md, c)
ykakarap marked this conversation as resolved.
Show resolved Hide resolved
Expect(err).To(BeNil())

// Scale up the MachineDeployment.
// IMPORTANT: Since the MachineDeployment is pending an upgrade at this point the topology controller will not push any changes
// to the MachineDeployment. Therefore, the changes made to the MachineDeployment here will not be replaced
// until the AfterControlPlaneUpgrade hook unblocks the upgrade.
*md.Spec.Replicas++
Eventually(func() error {
return patchHelper.Patch(ctx, md)
}).Should(Succeed(), "Failed to scale up the MachineDeployment %s", klog.KObj(md))
sbueringer marked this conversation as resolved.
Show resolved Hide resolved
// Verify the MachineDeployment updated replicas are not overridden by the topology controller.
// Note: This verifies that the topology controller in fact holds any reconciliation of this MachineDeployment.
Consistently(func(g Gomega) {
// Get the updated MachineDeployment.
targetMD := &clusterv1.MachineDeployment{}
// Wrap in an Eventually block for additional safety. Since all of this is in a Consistently block it
// will fail if we hit a transient error like a network flake.
g.Eventually(func() error {
return c.Get(ctx, client.ObjectKeyFromObject(md), targetMD)
}).Should(Succeed(), "Failed to get MachineDeployment %s", klog.KObj(md))
// Verify replicas are not overridden.
g.Expect(targetMD.Spec.Replicas).To(Equal(md.Spec.Replicas))
}, 10*time.Second, 1*time.Second)

// Since the MachineDeployment is scaled up (overriding the topology controller) at this point the MachineSet would
// also scale up. However, a new Machine creation would be blocked by one of the MachineSet preflight checks (KubeadmVersionSkew).
// Verify the MachineSet is blocking new Machine creation.
Eventually(func(g Gomega) {
machineSets := framework.GetMachineSetsByDeployment(ctx, framework.GetMachineSetsByDeploymentInput{
Lister: c,
MDName: md.Name,
Namespace: md.Namespace,
})
g.Expect(conditions.IsFalse(machineSets[0], clusterv1.MachinesCreatedCondition)).To(BeTrue())
machinesCreatedCondition := conditions.Get(machineSets[0], clusterv1.MachinesCreatedCondition)
g.Expect(machinesCreatedCondition).NotTo(BeNil())
g.Expect(machinesCreatedCondition.Reason).To(Equal(clusterv1.PreflightCheckFailedReason))
g.Expect(machineSets[0].Spec.Replicas).To(Equal(md.Spec.Replicas))
}).Should(Succeed(), "New Machine creation not blocked by MachineSet preflight checks")

// Verify that the MachineSet is not creating the new Machine.
// No new machines should be created for this MachineDeployment even though it is scaled up.
// Creation of new Machines will be blocked by MachineSet preflight checks (KubeadmVersionSkew).
Consistently(func(g Gomega) {
originalReplicas := int(*md.Spec.Replicas - 1)
machines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{
Lister: c,
ClusterName: clusterRef.Name,
Namespace: clusterRef.Namespace,
MachineDeployment: *md,
})
g.Expect(machines).To(HaveLen(originalReplicas), "New Machines should not be created")
}, 10*time.Second, time.Second)

// Scale down the MachineDeployment to the original replicas to restore to the state of the MachineDeployment
// it existed in before this test block.
patchHelper, err = patch.NewHelper(md, c)
Expect(err).To(BeNil())
*md.Spec.Replicas--
Eventually(func() error {
return patchHelper.Patch(ctx, md)
}).Should(Succeed(), "Failed to scale down the MachineDeployment %s", klog.KObj(md))
}

// extensionConfig generates an ExtensionConfig.
// We make sure this cluster-wide object does not conflict with others by using a random generated
// name and a NamespaceSelector selecting on the namespace of the current test.
Expand Down
50 changes: 50 additions & 0 deletions test/framework/machineset_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package framework

import (
"context"

. "github.com/onsi/gomega"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"

clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
)

// GetMachineSetsByDeploymentInput is the input for GetMachineSetsByDeployment.
type GetMachineSetsByDeploymentInput struct {
Lister Lister
MDName string
Namespace string
}

// GetMachineSetsByDeployment returns the MachineSets objects for a MachineDeployment.
// Important! this method relies on labels that are created by the CAPI controllers during the first reconciliation, so
// it is necessary to ensure this is already happened before calling it.
func GetMachineSetsByDeployment(ctx context.Context, input GetMachineSetsByDeploymentInput) []*clusterv1.MachineSet {
machineSetList := &clusterv1.MachineSetList{}
Eventually(func() error {
return input.Lister.List(ctx, machineSetList, client.InNamespace(input.Namespace), client.MatchingLabels{clusterv1.MachineDeploymentNameLabel: input.MDName})
}, retryableOperationTimeout, retryableOperationInterval).Should(Succeed(), "Failed to list MachineSets for MachineDeployment %s", klog.KRef(input.Namespace, input.MDName))

machineSets := make([]*clusterv1.MachineSet, len(machineSetList.Items))
for i := range machineSetList.Items {
machineSets[i] = &machineSetList.Items[i]
}
return machineSets
}