Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e5f7889

Browse files
committedMar 10, 2025··
fixup
1 parent dec1367 commit e5f7889

File tree

12 files changed

+310
-32
lines changed

12 files changed

+310
-32
lines changed
 

‎api/v1beta1/conversion.go

+11
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,14 @@ func GetK8sSecret(name, namespace string) (*corev1.Secret, error) {
174174
}
175175
return endpointCredentials, nil
176176
}
177+
178+
// Convert_v1beta3_Network_To_v1beta1_Network converts from v1beta3.Network to v1beta1.Network
179+
//
180+
//nolint:golint,revive,stylecheck
181+
func Convert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Network, _ conv.Scope) error {
182+
out.ID = in.ID
183+
out.Type = in.Type
184+
out.Name = in.Name
185+
// Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta1.Network
186+
return nil
187+
}

‎api/v1beta1/zz_generated.conversion.go

+5-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎api/v1beta2/conversion.go

+28
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,31 @@ limitations under the License.
1515
*/
1616

1717
package v1beta2
18+
19+
import (
20+
conv "k8s.io/apimachinery/pkg/conversion"
21+
"sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
22+
)
23+
24+
// Convert_v1beta3_Network_To_v1beta2_Network converts from v1beta3.Network to v1beta2.Network
25+
//
26+
//nolint:golint,revive,stylecheck
27+
func Convert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Network, _ conv.Scope) error {
28+
out.ID = in.ID
29+
out.Type = in.Type
30+
out.Name = in.Name
31+
// Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta2.Network
32+
return nil
33+
}
34+
35+
// Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec converts from v1beta3.CloudStackIsolatedNetworkSpec to v1beta2.CloudStackIsolatedNetworkSpec
36+
//
37+
//nolint:golint,revive,stylecheck
38+
func Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(in *v1beta3.CloudStackIsolatedNetworkSpec, out *CloudStackIsolatedNetworkSpec, _ conv.Scope) error {
39+
out.Name = in.Name
40+
out.ID = in.ID
41+
out.ControlPlaneEndpoint = in.ControlPlaneEndpoint
42+
out.FailureDomainName = in.FailureDomainName
43+
// Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta2.CloudStackIsolatedNetworkSpec
44+
return nil
45+
}

‎api/v1beta2/zz_generated.conversion.go

+10-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/cloud/isolated_network.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,13 @@ func (c *client) GetOrCreateIsolatedNetwork(
311311
return errors.Wrapf(err, "tagging network with id %s", networkID)
312312
}
313313

314+
// Tag the created VPC.
315+
if net.VPC != nil && net.VPC.ID != "" {
316+
if err := c.AddClusterTag(ResourceTypeVPC, net.VPC.ID, csCluster); err != nil {
317+
return errors.Wrapf(err, "tagging VPC with id %s", net.VPC.ID)
318+
}
319+
}
320+
314321
// Associate Public IP with CloudStackIsolatedNetwork
315322
if err := c.AssociatePublicIPAddress(fd, isoNet, csCluster); err != nil {
316323
return errors.Wrapf(err, "associating public IP address to csCluster")
@@ -372,7 +379,13 @@ func (c *client) DisposeIsoNetResources(
372379
if err := c.RemoveClusterTagFromNetwork(csCluster, *isoNet.Network()); err != nil {
373380
return err
374381
}
375-
err := c.DeleteNetworkIfNotInUse(*isoNet.Network())
382+
if err := c.RemoveClusterTagFromVPC(csCluster, *isoNet.Network()); err != nil {
383+
return err
384+
}
385+
if err := c.DeleteNetworkIfNotInUse(*isoNet.Network()); err != nil {
386+
return err
387+
}
388+
err := c.DeleteVPCIfNotInUse(isoNet.Spec.VPC)
376389
return err
377390
}
378391

‎pkg/cloud/vpc.go

+59-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package cloud
1818

1919
import (
20+
"strings"
21+
2022
infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3"
2123

2224
"github.com/apache/cloudstack-go/v2/cloudstack"
@@ -25,14 +27,15 @@ import (
2527

2628
// ResourceTypeVPC is the type identifier for VPC resources.
2729
const (
28-
ResourceTypeVPC = "VPC"
30+
ResourceTypeVPC = "Vpc"
2931
VPCOffering = "Default VPC offering"
3032
)
3133

3234
// VPCIface defines the interface for VPC operations.
3335
type VPCIface interface {
3436
ResolveVPC(*infrav1.VPC) error
3537
CreateVPC(*infrav1.CloudStackFailureDomain, *infrav1.VPC) error
38+
RemoveClusterTagFromVPC(*infrav1.CloudStackCluster, infrav1.Network) error
3639
}
3740

3841
// getVPCOfferingID fetches a vpc offering id.
@@ -100,5 +103,60 @@ func (c *client) CreateVPC(fd *infrav1.CloudStackFailureDomain, vpc *infrav1.VPC
100103
return errors.Wrapf(err, "creating VPC with name %s", vpc.Name)
101104
}
102105
vpc.ID = resp.Id
106+
return c.AddCreatedByCAPCTag(ResourceTypeVPC, vpc.ID)
107+
}
108+
109+
// DeleteVPC deletes a VPC.
110+
func (c *client) DeleteVPC(vpc *infrav1.VPC) error {
111+
_, err := c.cs.VPC.DeleteVPC(c.cs.VPC.NewDeleteVPCParams(vpc.ID))
112+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
113+
return errors.Wrapf(err, "deleting vpc with id %s", vpc.ID)
114+
}
115+
116+
// DeleteVPCIfNotInUse deletes a VPC if the VPC is no longer in use (indicated by in use tags).
117+
func (c *client) DeleteVPCIfNotInUse(vpc *infrav1.VPC) (retError error) {
118+
if vpc == nil || vpc.ID == "" {
119+
return nil
120+
}
121+
tags, err := c.GetTags(ResourceTypeVPC, vpc.ID)
122+
if err != nil {
123+
return err
124+
}
125+
126+
var clusterTagCount int
127+
for tagName := range tags {
128+
if strings.HasPrefix(tagName, ClusterTagNamePrefix) {
129+
clusterTagCount++
130+
}
131+
}
132+
133+
if clusterTagCount == 0 && tags[CreatedByCAPCTagName] != "" {
134+
return c.DeleteVPC(vpc)
135+
}
136+
137+
return nil
138+
}
139+
140+
func generateVPCTagName(csCluster *infrav1.CloudStackCluster) string {
141+
return ClusterTagNamePrefix + string(csCluster.UID)
142+
}
143+
144+
// RemoveClusterTagFromNetwork the cluster in use tag from a network.
145+
func (c *client) RemoveClusterTagFromVPC(csCluster *infrav1.CloudStackCluster, net infrav1.Network) (retError error) {
146+
if net.VPC == nil || net.VPC.ID == "" {
147+
return nil
148+
}
149+
tags, err := c.GetTags(ResourceTypeVPC, net.VPC.ID)
150+
if err != nil {
151+
return err
152+
}
153+
154+
ClusterTagName := generateVPCTagName(csCluster)
155+
if tagValue := tags[ClusterTagName]; tagValue != "" {
156+
if err = c.DeleteTags(ResourceTypeNetwork, net.ID, map[string]string{ClusterTagName: tagValue}); err != nil {
157+
return err
158+
}
159+
}
160+
103161
return nil
104162
}

‎test/e2e/common.go

+13
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,19 @@ func CheckNetworkExists(client *cloudstack.CloudStackClient, networkName string)
393393
return count == 1, nil
394394
}
395395

396+
func CheckVPCExists(client *cloudstack.CloudStackClient, vpcName string) (bool, error) {
397+
_, count, err := client.VPC.GetVPCByName(vpcName)
398+
if err != nil {
399+
if strings.Contains(err.Error(), "No match found for") {
400+
return false, nil
401+
}
402+
return false, err
403+
} else if count > 1 {
404+
return false, fmt.Errorf("Expected 0-1 VPC with name %s, but got %d.", vpcName, count)
405+
}
406+
return count == 1, nil
407+
}
408+
396409
func CreateCloudStackClient(ctx context.Context, kubeConfigPath string) *cloudstack.CloudStackClient {
397410
By("Getting a CloudStack client secret")
398411
secret := &corev1.Secret{}

‎test/e2e/config/cloudstack.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ providers:
8888
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-resource-cleanup.yaml"
8989
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-second-cluster.yaml"
9090
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-shared-network-kubevip.yaml"
91+
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network.yaml"
9192
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering.yaml"
9293
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering-size-for-non-customized.yaml"
9394
- sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering-size-for-customized.yaml"
@@ -129,6 +130,10 @@ variables:
129130
CLOUDSTACK_DOMAIN_NAME: ROOT
130131
CLOUDSTACK_INVALID_DOMAIN_NAME: domainXXXX
131132
CLOUDSTACK_NETWORK_NAME: isolated-for-e2e-1
133+
CLOUDSTACK_VPC_NAME: vpc-for-e2e-1
134+
CLOUDSTACK_VPC_CIDR: 10.10.0.0/16
135+
CLOUDSTACK_GATEWAY: 10.10.0.1
136+
CLOUDSTACK_NETMASK: 255.255.255.0
132137
CLOUDSTACK_NEW_NETWORK_NAME: isolated-for-e2e-new
133138
CLOUDSTACK_SHARED_NETWORK_NAME: Shared1
134139
CLUSTER_ENDPOINT_IP: 172.16.2.199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
apiVersion: infrastructure.cluster.x-k8s.io/v1beta3
3+
kind: CloudStackCluster
4+
metadata:
5+
name: ${CLUSTER_NAME}
6+
spec:
7+
failureDomains:
8+
- name: ${CLOUDSTACK_FD1_NAME}
9+
acsEndpoint:
10+
name: ${CLOUDSTACK_FD1_SECRET_NAME}
11+
namespace: default
12+
zone:
13+
name: ${CLOUDSTACK_ZONE_NAME}
14+
network:
15+
name: ${CLOUDSTACK_NETWORK_NAME}
16+
gateway: ${CLOUDSTACK_GATEWAY}
17+
netmask: ${CLOUDSTACK_NETMASK}
18+
vpc:
19+
name: ${CLOUDSTACK_VPC_NAME}
20+
cidr: ${CLOUDSTACK_VPC_CIDR}
21+
controlPlaneEndpoint:
22+
host: ""
23+
port: 6443
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bases:
2+
- ../bases/cluster-with-kcp.yaml
3+
- ../bases/md.yaml
4+
5+
patchesStrategicMerge:
6+
- ./cluster-with-vpc-network.yaml

‎test/e2e/vpc_network.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 e2e
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
25+
. "github.com/onsi/ginkgo/v2"
26+
. "github.com/onsi/gomega"
27+
corev1 "k8s.io/api/core/v1"
28+
"k8s.io/utils/pointer"
29+
30+
"sigs.k8s.io/cluster-api/test/framework/clusterctl"
31+
"sigs.k8s.io/cluster-api/util"
32+
)
33+
34+
// VPCNetworkSpec implements a test that verifies cluster creation in a VPC network.
35+
func VPCNetworkSpec(ctx context.Context, inputGetter func() CommonSpecInput) {
36+
var (
37+
specName = "vpc-network"
38+
input CommonSpecInput
39+
namespace *corev1.Namespace
40+
cancelWatches context.CancelFunc
41+
clusterResources *clusterctl.ApplyClusterTemplateAndWaitResult
42+
vpcName = "vpc-for-e2e-1"
43+
)
44+
45+
BeforeEach(func() {
46+
Expect(ctx).NotTo(BeNil(), "ctx is required for %s spec", specName)
47+
input = inputGetter()
48+
Expect(input.E2EConfig).ToNot(BeNil(), "Invalid argument. input.E2EConfig can't be nil when calling %s spec", specName)
49+
Expect(input.ClusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. input.ClusterctlConfigPath must be an existing file when calling %s spec", specName)
50+
Expect(input.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName)
51+
Expect(os.MkdirAll(input.ArtifactFolder, 0750)).To(Succeed(), "Invalid argument. input.ArtifactFolder can't be created for %s spec", specName)
52+
Expect(input.E2EConfig.Variables).To(HaveKey(KubernetesVersion))
53+
Expect(input.E2EConfig.Variables).To(HaveValidVersion(input.E2EConfig.GetVariable(KubernetesVersion)))
54+
55+
// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
56+
namespace, cancelWatches = setupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder)
57+
clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult)
58+
})
59+
60+
It("Should successfully create a cluster in a VPC network", func() {
61+
By("Creating a workload cluster in a VPC network")
62+
63+
clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
64+
ClusterProxy: input.BootstrapClusterProxy,
65+
CNIManifestPath: input.E2EConfig.GetVariable(CNIPath),
66+
ConfigCluster: clusterctl.ConfigClusterInput{
67+
LogFolder: filepath.Join(input.ArtifactFolder, "clusters", input.BootstrapClusterProxy.GetName()),
68+
ClusterctlConfigPath: input.ClusterctlConfigPath,
69+
KubeconfigPath: input.BootstrapClusterProxy.GetKubeconfigPath(),
70+
InfrastructureProvider: clusterctl.DefaultInfrastructureProvider,
71+
Flavor: specName,
72+
Namespace: namespace.Name,
73+
ClusterName: fmt.Sprintf("%s-%s", specName, util.RandomString(6)),
74+
KubernetesVersion: input.E2EConfig.GetVariable(KubernetesVersion),
75+
ControlPlaneMachineCount: pointer.Int64Ptr(3),
76+
WorkerMachineCount: pointer.Int64Ptr(2),
77+
},
78+
WaitForClusterIntervals: input.E2EConfig.GetIntervals(specName, "wait-cluster"),
79+
WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"),
80+
WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"),
81+
}, clusterResources)
82+
83+
csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath())
84+
85+
exists, err := CheckVPCExists(csClient, vpcName)
86+
Expect(err).To(BeNil())
87+
Expect(exists).To(BeTrue())
88+
89+
By("PASSED!")
90+
})
91+
92+
AfterEach(func() {
93+
// Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself.
94+
dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup)
95+
})
96+
}

‎test/e2e/vpc_network_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//go:build e2e
2+
// +build e2e
3+
4+
/*
5+
Copyright 2023 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package e2e
21+
22+
import (
23+
"context"
24+
25+
. "github.com/onsi/ginkgo/v2"
26+
)
27+
28+
var _ = Describe("When testing clusters in a VPC network", func() {
29+
30+
VPCNetworkSpec(context.TODO(), func() CommonSpecInput {
31+
return CommonSpecInput{
32+
E2EConfig: e2eConfig,
33+
ClusterctlConfigPath: clusterctlConfigPath,
34+
BootstrapClusterProxy: bootstrapClusterProxy,
35+
ArtifactFolder: artifactFolder,
36+
SkipCleanup: skipCleanup,
37+
}
38+
})
39+
40+
})

0 commit comments

Comments
 (0)
Please sign in to comment.