Skip to content

Commit b7850b1

Browse files
authored
Merge pull request #132 from mrog/main
Added e2e tests for upgrading clusters with insufficient compute resources
2 parents e4c0653 + b2405ce commit b7850b1

File tree

6 files changed

+221
-41
lines changed

6 files changed

+221
-41
lines changed

test/e2e/common.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,18 @@ import (
4747

4848
// Test suite constants for e2e config variables.
4949
const (
50-
KubernetesVersionManagement = "KUBERNETES_VERSION_MANAGEMENT"
51-
KubernetesVersion = "KUBERNETES_VERSION"
52-
CNIPath = "CNI"
53-
CNIResources = "CNI_RESOURCES"
54-
IPFamily = "IP_FAMILY"
55-
InvalidZoneName = "CLOUDSTACK_INVALID_ZONE_NAME"
56-
InvalidDiskOfferingName = "CLOUDSTACK_INVALID_DISK_OFFERING_NAME"
57-
InvalidNetworkName = "CLOUDSTACK_INVALID_NETWORK_NAME"
58-
InvalidAccountName = "CLOUDSTACK_INVALID_ACCOUNT_NAME"
59-
InvalidDomainName = "CLOUDSTACK_INVALID_DOMAIN_NAME"
60-
InvalidTemplateName = "CLOUDSTACK_INVALID_TEMPLATE_NAME"
61-
InvalidCPOfferingName = "CLOUDSTACK_INVALID_CONTROL_PLANE_MACHINE_OFFERING"
62-
ExtremelyLargeCPOfferingName = "CLOUDSTACK_EXTREMELY_LARGE_CONTROL_PLANE_MACHINE_OFFERING"
50+
KubernetesVersionManagement = "KUBERNETES_VERSION_MANAGEMENT"
51+
KubernetesVersion = "KUBERNETES_VERSION"
52+
CNIPath = "CNI"
53+
CNIResources = "CNI_RESOURCES"
54+
IPFamily = "IP_FAMILY"
55+
InvalidZoneName = "CLOUDSTACK_INVALID_ZONE_NAME"
56+
InvalidDiskOfferingName = "CLOUDSTACK_INVALID_DISK_OFFERING_NAME"
57+
InvalidNetworkName = "CLOUDSTACK_INVALID_NETWORK_NAME"
58+
InvalidAccountName = "CLOUDSTACK_INVALID_ACCOUNT_NAME"
59+
InvalidDomainName = "CLOUDSTACK_INVALID_DOMAIN_NAME"
60+
InvalidTemplateName = "CLOUDSTACK_INVALID_TEMPLATE_NAME"
61+
InvalidCPOfferingName = "CLOUDSTACK_INVALID_CONTROL_PLANE_MACHINE_OFFERING"
6362
)
6463

6564
const (

test/e2e/config/cloudstack.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ providers:
8080
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-invalid-template.yaml"
8181
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-invalid-cp-offering.yaml"
8282
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-insufficient-compute-resources.yaml"
83+
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-insufficient-compute-resources-for-upgrade.yaml"
8384
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-node-drain.yaml"
8485
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-machine-remediation.yaml"
8586
- sourcePath: "../data/infrastructure-cloudstack/v1beta1/cluster-template-affinity-group-pro.yaml"
@@ -125,8 +126,9 @@ variables:
125126
CLUSTER_ENDPOINT_IP_2: 172.16.2.198
126127
CLOUDSTACK_CONTROL_PLANE_MACHINE_OFFERING: "Large Instance"
127128
CLOUDSTACK_INVALID_CONTROL_PLANE_MACHINE_OFFERING: "OfferingXXXX"
128-
CLOUDSTACK_EXTREMELY_LARGE_CONTROL_PLANE_MACHINE_OFFERING: "Extremely Large Instance"
129+
CLOUDSTACK_IMPOSSIBLE_CONTROL_PLANE_MACHINE_OFFERING: "Impossible Instance"
129130
CLOUDSTACK_WORKER_MACHINE_OFFERING: "Medium Instance"
131+
CLOUDSTACK_IMPOSSIBLE_WORKER_MACHINE_OFFERING: "Impossible Instance"
130132
CLOUDSTACK_TEMPLATE_NAME: kube-v1.20.10/ubuntu-2004
131133
CLOUDSTACK_INVALID_TEMPLATE_NAME: templateXXXX
132134
CLOUDSTACK_SSH_KEY_NAME: CAPCKeyPair6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
bases:
2+
- ../bases/cluster-with-kcp.yaml
3+
- ../bases/md.yaml
4+
- upgrade.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
2+
kind: CloudStackMachineTemplate
3+
metadata:
4+
name: ${CLUSTER_NAME}-upgrade-control-plane
5+
spec:
6+
template:
7+
spec:
8+
offering:
9+
name: ${CLOUDSTACK_IMPOSSIBLE_CONTROL_PLANE_MACHINE_OFFERING}
10+
template:
11+
name: ${CLOUDSTACK_TEMPLATE_NAME}
12+
sshKey: ${CLOUDSTACK_SSH_KEY_NAME}
13+
---
14+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
15+
kind: CloudStackMachineTemplate
16+
metadata:
17+
name: ${CLUSTER_NAME}-upgrade-md-0
18+
spec:
19+
template:
20+
spec:
21+
offering:
22+
name: ${CLOUDSTACK_IMPOSSIBLE_WORKER_MACHINE_OFFERING}
23+
template:
24+
name: ${CLOUDSTACK_TEMPLATE_NAME}
25+
sshKey: ${CLOUDSTACK_SSH_KEY_NAME}

test/e2e/data/infrastructure-cloudstack/v1beta1/cluster-template-insufficient-compute-resources/md.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ spec:
66
template:
77
spec:
88
offering:
9-
name: ${CLOUDSTACK_EXTREMELY_LARGE_CONTROL_PLANE_MACHINE_OFFERING}
9+
name: ${CLOUDSTACK_IMPOSSIBLE_CONTROL_PLANE_MACHINE_OFFERING}
1010
template:
1111
name: ${CLOUDSTACK_TEMPLATE_NAME}
1212
sshKey: ${CLOUDSTACK_SSH_KEY_NAME}

test/e2e/invalid_resource.go

Lines changed: 176 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23-
"os"
24-
"path/filepath"
25-
"strings"
26-
2723
. "github.com/onsi/ginkgo"
2824
. "github.com/onsi/gomega"
29-
3025
corev1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3127
"k8s.io/utils/pointer"
28+
"os"
29+
"path/filepath"
30+
"sigs.k8s.io/cluster-api/api/v1beta1"
31+
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
32+
"sigs.k8s.io/cluster-api/util/patch"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"strings"
3235

3336
"sigs.k8s.io/cluster-api/test/framework"
3437
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
@@ -45,7 +48,6 @@ var (
4548

4649
// InvalidResourceSpec implements a test that verifies that creating a new cluster fails when the specified resource does not exist
4750
func InvalidResourceSpec(ctx context.Context, inputGetter func() CommonSpecInput) {
48-
4951
BeforeEach(func() {
5052
Expect(ctx).NotTo(BeNil(), "ctx is required for %s spec", specName)
5153
input = inputGetter()
@@ -97,6 +99,65 @@ func InvalidResourceSpec(ctx context.Context, inputGetter func() CommonSpecInput
9799
testInvalidResource(ctx, input, "invalid-disk-offering-size-for-customized", "is customized, disk size can not be 0 GB")
98100
})
99101

102+
Context("When starting with a healthy cluster", func() {
103+
var logFolder string
104+
105+
BeforeEach(func() {
106+
logFolder = generateLogFolderPath()
107+
108+
By("Creating a workload cluster")
109+
clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
110+
ClusterProxy: input.BootstrapClusterProxy,
111+
CNIManifestPath: input.E2EConfig.GetVariable(CNIPath),
112+
ConfigCluster: clusterctl.ConfigClusterInput{
113+
LogFolder: logFolder,
114+
ClusterctlConfigPath: input.ClusterctlConfigPath,
115+
KubeconfigPath: input.BootstrapClusterProxy.GetKubeconfigPath(),
116+
InfrastructureProvider: clusterctl.DefaultInfrastructureProvider,
117+
Flavor: "insufficient-compute-resources-for-upgrade",
118+
Namespace: namespace.Name,
119+
ClusterName: generateClusterName(),
120+
KubernetesVersion: input.E2EConfig.GetVariable(KubernetesVersion),
121+
ControlPlaneMachineCount: pointer.Int64Ptr(1),
122+
WorkerMachineCount: pointer.Int64Ptr(1),
123+
},
124+
WaitForClusterIntervals: input.E2EConfig.GetIntervals(specName, "wait-cluster"),
125+
WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"),
126+
WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
127+
}, clusterResources)
128+
})
129+
130+
It("Should fail to upgrade worker machine due to insufficient compute resources", func() {
131+
By("Making sure the expected error didn't occur yet")
132+
expectedError := "Unable to create a deployment for VM"
133+
Expect(errorExistsInLog(logFolder, expectedError)).To(BeFalse())
134+
135+
By("Increasing the machine deployment instance size")
136+
deployment := clusterResources.MachineDeployments[0]
137+
deployment.Spec.Template.Spec.InfrastructureRef.Name =
138+
strings.Replace(deployment.Spec.Template.Spec.InfrastructureRef.Name, "-md-0", "-upgrade-md-0", 1)
139+
upgradeMachineDeploymentInfrastructureRef(ctx, deployment)
140+
141+
By("Checking for the expected error")
142+
waitForErrorInLog(logFolder, expectedError)
143+
})
144+
145+
It("Should fail to upgrade control plane machine due to insufficient compute resources", func() {
146+
By("Making sure the expected error didn't occur yet")
147+
expectedError := "Unable to create a deployment for VM"
148+
Expect(errorExistsInLog(logFolder, expectedError)).To(BeFalse())
149+
150+
By("Increasing the machine deployment instance size")
151+
cp := clusterResources.ControlPlane
152+
cp.Spec.MachineTemplate.InfrastructureRef.Name =
153+
strings.Replace(cp.Spec.MachineTemplate.InfrastructureRef.Name, "-control-plane", "-upgrade-control-plane", 1)
154+
upgradeControlPlaneInfrastructureRef(ctx, cp)
155+
156+
By("Checking for the expected error")
157+
waitForErrorInLog(logFolder, expectedError)
158+
})
159+
})
160+
100161
AfterEach(func() {
101162
// Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself.
102163
dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup)
@@ -105,8 +166,8 @@ func InvalidResourceSpec(ctx context.Context, inputGetter func() CommonSpecInput
105166
}
106167

107168
func testInvalidResource(ctx context.Context, input CommonSpecInput, flavor string, expectedError string) {
108-
logFolder := filepath.Join(input.ArtifactFolder, "clusters", input.BootstrapClusterProxy.GetName())
109-
clusterName := fmt.Sprintf("%s-%s", specName, util.RandomString(6))
169+
logFolder := generateLogFolderPath()
170+
clusterName := generateClusterName()
110171

111172
By("Configuring a cluster")
112173
workloadClusterTemplate := clusterctl.ConfigCluster(ctx, clusterctl.ConfigClusterInput{
@@ -131,27 +192,116 @@ func testInvalidResource(ctx context.Context, input CommonSpecInput, flavor stri
131192
Namespace: namespace.Name,
132193
})
133194

134-
Byf("Waiting for %q error to occur", expectedError)
135-
Eventually(func() (string, error) {
136-
err := filepath.Walk(logFolder, func(path string, info os.FileInfo, err error) error {
137-
if err != nil {
138-
return err
139-
}
140-
if strings.Contains(path, "capc-controller-manager") && strings.Contains(path, "manager.log") {
141-
log, _ := os.ReadFile(path)
142-
if strings.Contains(string(log), expectedError) {
195+
waitForErrorInLog(logFolder, expectedError)
196+
197+
By("PASSED!")
198+
}
199+
200+
func generateLogFolderPath() string {
201+
return filepath.Join(input.ArtifactFolder, "clusters", input.BootstrapClusterProxy.GetName())
202+
}
203+
204+
func generateClusterName() string {
205+
return fmt.Sprintf("%s-%s", specName, util.RandomString(6))
206+
}
207+
208+
// errorExistsInLog looks for a specific error message in the CAPC controller log files. Because the logs may contain
209+
// entries from previous test runs, or from previous tests in the same run, the function also requires that the log
210+
// line contains the namespace and cluster names.
211+
func errorExistsInLog(logFolder string, expectedError string) (bool, error) {
212+
expectedErrorFound := errors.New("expected error found")
213+
controllerLogPath := filepath.Join(logFolder, "controllers", "capc-controller-manager")
214+
215+
err := filepath.Walk(controllerLogPath, func(path string, info os.FileInfo, err error) error {
216+
if err != nil {
217+
return err
218+
}
219+
220+
if strings.Contains(path, "manager.log") {
221+
log, _ := os.ReadFile(path)
222+
logLines := strings.Split(string(log), "\n")
223+
for _, line := range logLines {
224+
if strings.Contains(line, expectedError) &&
225+
strings.Contains(line, clusterResources.Cluster.Namespace) &&
226+
strings.Contains(line, clusterResources.Cluster.Name) {
143227
Byf("Found %q error", expectedError)
144-
return errors.New("expected error found")
228+
return expectedErrorFound
145229
}
146230
}
147-
return nil
148-
})
149-
if err == nil {
150-
return "expected error not found", nil
151-
} else {
152-
return err.Error(), nil
153231
}
154-
}, input.E2EConfig.GetIntervals(specName, "wait-errors")...).Should(Equal(string("expected error found")))
155232

156-
By("PASSED!")
233+
return nil
234+
})
235+
236+
if err == nil {
237+
return false, nil
238+
} else if err == expectedErrorFound {
239+
return true, nil
240+
}
241+
return false, err
242+
}
243+
244+
func waitForErrorInLog(logFolder string, expectedError string) {
245+
Byf("Waiting for %q error to occur", expectedError)
246+
Eventually(func() (bool, error) {
247+
return errorExistsInLog(logFolder, expectedError)
248+
}, input.E2EConfig.GetIntervals(specName, "wait-errors")...).Should(BeTrue())
249+
}
250+
251+
// upgradeMachineDeploymentInfrastructureRef updates a machine deployment infrastructure ref and returns immediately.
252+
// The logic was borrowed from framework.UpgradeMachineDeploymentInfrastructureRefAndWait.
253+
func upgradeMachineDeploymentInfrastructureRef(ctx context.Context, deployment *v1beta1.MachineDeployment) {
254+
By("Patching the machine deployment infrastructure ref")
255+
mgmtClient := input.BootstrapClusterProxy.GetClient()
256+
257+
// Create a new infrastructure ref based on the existing one
258+
infraRef := deployment.Spec.Template.Spec.InfrastructureRef
259+
newInfraObjName := createNewInfrastructureRef(ctx, infraRef)
260+
261+
// Patch the new infra object's ref to the machine deployment
262+
patchHelper, err := patch.NewHelper(deployment, mgmtClient)
263+
Expect(err).ToNot(HaveOccurred())
264+
infraRef.Name = newInfraObjName
265+
deployment.Spec.Template.Spec.InfrastructureRef = infraRef
266+
Expect(patchHelper.Patch(ctx, deployment)).To(Succeed())
267+
}
268+
269+
// upgradeControlPlane upgrades a control plane deployment infrastructure ref and returns immediately.
270+
func upgradeControlPlaneInfrastructureRef(ctx context.Context, controlPlane *controlplanev1.KubeadmControlPlane) {
271+
By("Patching the control plane infrastructure ref")
272+
mgmtClient := input.BootstrapClusterProxy.GetClient()
273+
274+
// Create a new infrastructure ref based on the existing one
275+
infraRef := controlPlane.Spec.MachineTemplate.InfrastructureRef
276+
newInfraObjName := createNewInfrastructureRef(ctx, infraRef)
277+
278+
// Patch the control plane to use the new infrastructure ref
279+
patchHelper, err := patch.NewHelper(controlPlane, mgmtClient)
280+
Expect(err).ToNot(HaveOccurred())
281+
infraRef.Name = newInfraObjName
282+
controlPlane.Spec.MachineTemplate.InfrastructureRef = infraRef
283+
Expect(patchHelper.Patch(ctx, controlPlane)).To(Succeed())
284+
}
285+
286+
// createNewInfrastructureRef creates a new infrastructure ref that's based on an existing one, but has a new name. The
287+
// new name is returned.
288+
func createNewInfrastructureRef(ctx context.Context, sourceInfrastructureRef corev1.ObjectReference) string {
289+
mgmtClient := input.BootstrapClusterProxy.GetClient()
290+
291+
// Retrieve the existing infrastructure ref object
292+
infraObj := &unstructured.Unstructured{}
293+
infraObj.SetGroupVersionKind(sourceInfrastructureRef.GroupVersionKind())
294+
key := client.ObjectKey{
295+
Namespace: clusterResources.Cluster.Namespace,
296+
Name: sourceInfrastructureRef.Name,
297+
}
298+
Expect(mgmtClient.Get(ctx, key, infraObj)).NotTo(HaveOccurred())
299+
300+
// Creates a new infrastructure ref object
301+
newInfraObj := infraObj
302+
newInfraObjName := fmt.Sprintf("%s-%s", sourceInfrastructureRef.Name, util.RandomString(6))
303+
newInfraObj.SetName(newInfraObjName)
304+
newInfraObj.SetResourceVersion("")
305+
Expect(mgmtClient.Create(ctx, newInfraObj)).NotTo(HaveOccurred())
306+
return newInfraObjName
157307
}

0 commit comments

Comments
 (0)