From 00df013383958b71c430071c823eaee59b685299 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Tue, 4 Mar 2025 19:44:16 +0530 Subject: [PATCH 1/4] Add VPC Support --- api/v1beta3/cloudstackfailuredomain_types.go | 14 +++ .../cloudstackisolatednetwork_types.go | 7 +- api/v1beta3/zz_generated.deepcopy.go | 17 +++ ...e.cluster.x-k8s.io_cloudstackclusters.yaml | 10 ++ ...ter.x-k8s.io_cloudstackfailuredomains.yaml | 10 ++ ...r.x-k8s.io_cloudstackisolatednetworks.yaml | 10 ++ pkg/cloud/client.go | 1 + pkg/cloud/isolated_network.go | 58 ++++++++- pkg/cloud/network.go | 8 ++ pkg/cloud/vpc.go | 113 ++++++++++++++++++ 10 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 pkg/cloud/vpc.go diff --git a/api/v1beta3/cloudstackfailuredomain_types.go b/api/v1beta3/cloudstackfailuredomain_types.go index d3af6fe7..40a93c15 100644 --- a/api/v1beta3/cloudstackfailuredomain_types.go +++ b/api/v1beta3/cloudstackfailuredomain_types.go @@ -53,6 +53,20 @@ type Network struct { // Cloudstack Network Name the cluster is built in. Name string `json:"name"` + + // Cloudstack VPC the network belongs to. + // +optional + VPC VPC `json:"vpc,omitempty"` +} + +type VPC struct { + // Cloudstack VPC ID of the network. + // +optional + ID string `json:"id,omitempty"` + + // Cloudstack VPC Name of the network. + // +optional + Name string `json:"name"` } // CloudStackZoneSpec specifies a Zone's details. diff --git a/api/v1beta3/cloudstackisolatednetwork_types.go b/api/v1beta3/cloudstackisolatednetwork_types.go index 76c15995..dddd3565 100644 --- a/api/v1beta3/cloudstackisolatednetwork_types.go +++ b/api/v1beta3/cloudstackisolatednetwork_types.go @@ -39,6 +39,10 @@ type CloudStackIsolatedNetworkSpec struct { // FailureDomainName -- the FailureDomain the network is placed in. FailureDomainName string `json:"failureDomainName"` + + // VPC the network belongs to. + // +optional + VPC VPC `json:"vpc,omitempty"` } // CloudStackIsolatedNetworkStatus defines the observed state of CloudStackIsolatedNetwork @@ -57,7 +61,8 @@ func (n *CloudStackIsolatedNetwork) Network() *Network { return &Network{ Name: n.Spec.Name, Type: "IsolatedNetwork", - ID: n.Spec.ID} + ID: n.Spec.ID, + VPC: n.Spec.VPC} } //+kubebuilder:object:root=true diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index a6919a22..e495ee1a 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -377,6 +377,7 @@ func (in *CloudStackIsolatedNetworkList) DeepCopyObject() runtime.Object { func (in *CloudStackIsolatedNetworkSpec) DeepCopyInto(out *CloudStackIsolatedNetworkSpec) { *out = *in out.ControlPlaneEndpoint = in.ControlPlaneEndpoint + out.VPC = in.VPC } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStackIsolatedNetworkSpec. @@ -774,6 +775,7 @@ func (in *CloudStackZoneSpec) DeepCopy() *CloudStackZoneSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Network) DeepCopyInto(out *Network) { *out = *in + out.VPC = in.VPC } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network. @@ -785,3 +787,18 @@ func (in *Network) DeepCopy() *Network { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPC) DeepCopyInto(out *VPC) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPC. +func (in *VPC) DeepCopy() *VPC { + if in == nil { + return nil + } + out := new(VPC) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml index 81bbca78..ed7cbae6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml @@ -403,6 +403,16 @@ spec: description: Cloudstack Network Type the cluster is built in. type: string + vpc: + description: Cloudstack VPC the network belongs to. + properties: + id: + description: Cloudstack VPC ID of the network. + type: string + name: + description: Cloudstack VPC Name of the network. + type: string + type: object required: - name type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml index 4dcb3292..f216d011 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml @@ -180,6 +180,16 @@ spec: description: Cloudstack Network Type the cluster is built in. type: string + vpc: + description: Cloudstack VPC the network belongs to. + properties: + id: + description: Cloudstack VPC ID of the network. + type: string + name: + description: Cloudstack VPC Name of the network. + type: string + type: object required: - name type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml index 35c0769d..cc4b3045 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml @@ -198,6 +198,16 @@ spec: name: description: Name. type: string + vpc: + description: VPC the network belongs to. + properties: + id: + description: Cloudstack VPC ID of the network. + type: string + name: + description: Cloudstack VPC Name of the network. + type: string + type: object required: - controlPlaneEndpoint - failureDomainName diff --git a/pkg/cloud/client.go b/pkg/cloud/client.go index 76c50256..f387288c 100644 --- a/pkg/cloud/client.go +++ b/pkg/cloud/client.go @@ -45,6 +45,7 @@ type Client interface { ZoneIFace IsoNetworkIface UserCredIFace + VPCIface NewClientInDomainAndAccount(string, string, string) (Client, error) } diff --git a/pkg/cloud/isolated_network.go b/pkg/cloud/isolated_network.go index 200e543b..5ff89144 100644 --- a/pkg/cloud/isolated_network.go +++ b/pkg/cloud/isolated_network.go @@ -67,8 +67,8 @@ func (c *client) AssociatePublicIPAddress( csCluster.Spec.ControlPlaneEndpoint.Host = publicAddress.Ipaddress isoNet.Status.PublicIPID = publicAddress.Id - // Check if the address is already associated with the network. - if publicAddress.Associatednetworkid == isoNet.Spec.ID { + // Check if the address is already associated with the network or VPC. + if publicAddress.Associatednetworkid == isoNet.Spec.ID || publicAddress.Vpcid == isoNet.Spec.VPC.ID { return nil } @@ -76,6 +76,9 @@ func (c *client) AssociatePublicIPAddress( p := c.cs.Address.NewAssociateIpAddressParams() p.SetIpaddress(isoNet.Spec.ControlPlaneEndpoint.Host) p.SetNetworkid(isoNet.Spec.ID) + if isoNet.Spec.VPC.ID != "" { + p.SetVpcid(isoNet.Spec.VPC.ID) + } setIfNotEmpty(c.user.Project.ID, p.SetProjectid) if _, err := c.cs.Address.AssociateIpAddress(p); err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -100,10 +103,61 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN return err } + // First, check if VPC is specified and handle it + if isoNet.Spec.VPC.Name != "" || isoNet.Spec.VPC.ID != "" { + // Try to resolve or create the VPC + err := c.ResolveVPC(&isoNet.Spec.VPC) + if err != nil { + return errors.Wrap(err, "resolving VPC for isolated network") + // } + + // TODO: Handle VPC creation + + // // VPC not found, need to create it + // // First, get a VPC offering ID + // vpcOfferingParams := c.cs.VPC.NewListVPCOfferingsParams() + // vpcOfferingResp, err := c.cs.VPC.ListVPCOfferings(vpcOfferingParams) + // if err != nil { + // c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + // return errors.Wrap(err, "listing VPC offerings") + // } + // if vpcOfferingResp.Count == 0 { + // return errors.New("no VPC offerings available") + // } + + // // Use the first VPC offering + // vpcOfferingID := vpcOfferingResp.VPCOfferings[0].Id + + // // Create the VPC + // vpcParams := c.cs.VPC.NewCreateVPCParams(isoNet.Spec.VPC.Name, vpcOfferingID, fd.Spec.Zone.ID) + // vpcParams.SetDisplaytext(isoNet.Spec.VPC.Name) + // setIfNotEmpty(c.user.Project.ID, vpcParams.SetProjectid) + + // vpcResp, err := c.cs.VPC.CreateVPC(vpcParams) + // if err != nil { + // c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + // return errors.Wrapf(err, "creating VPC with name %s", isoNet.Spec.VPC.Name) + // } + + // isoNet.Spec.VPC.ID = vpcResp.Id + + // // Tag the VPC + // if err := c.AddCreatedByCAPCTag(ResourceTypeVPC, isoNet.Spec.VPC.ID); err != nil { + // return errors.Wrapf(err, "tagging VPC with ID %s", isoNet.Spec.VPC.ID) + // } + } + } + // Do isolated network creation. p := c.cs.Network.NewCreateNetworkParams(isoNet.Spec.Name, offeringID, fd.Spec.Zone.ID) p.SetDisplaytext(isoNet.Spec.Name) setIfNotEmpty(c.user.Project.ID, p.SetProjectid) + + // If VPC is specified, set the VPC ID for the network + if isoNet.Spec.VPC.ID != "" { + p.SetVpcid(isoNet.Spec.VPC.ID) + } + resp, err := c.cs.Network.CreateNetwork(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) diff --git a/pkg/cloud/network.go b/pkg/cloud/network.go index 53637a70..053299fc 100644 --- a/pkg/cloud/network.go +++ b/pkg/cloud/network.go @@ -62,6 +62,10 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { } else { // Got netID from the network's name. net.ID = netDetails.Id net.Type = netDetails.Type + if netDetails.Vpcid != "" { + net.VPC.ID = netDetails.Vpcid + net.VPC.Name = netDetails.Vpcname + } return nil } @@ -76,6 +80,10 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { net.Name = netDetails.Name net.ID = netDetails.Id net.Type = netDetails.Type + if netDetails.Vpcid != "" { + net.VPC.ID = netDetails.Vpcid + net.VPC.Name = netDetails.Vpcname + } return nil } diff --git a/pkg/cloud/vpc.go b/pkg/cloud/vpc.go new file mode 100644 index 00000000..14c732fc --- /dev/null +++ b/pkg/cloud/vpc.go @@ -0,0 +1,113 @@ +/* +Copyright 2024 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 cloud + +import ( + "strings" + + infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/pkg/errors" +) + +// ResourceTypeVPC is the type identifier for VPC resources. +const ResourceTypeVPC = "VPC" + +// VPCIface defines the interface for VPC operations. +type VPCIface interface { + ResolveVPC(*infrav1.VPC) error + CreateVPC(*infrav1.VPC) error + GetOrCreateVPC(*infrav1.VPC) error +} + +// ResolveVPC checks if the specified VPC exists by ID or name. +// If it exists, it updates the VPC struct with the resolved ID or name. +func (c *client) ResolveVPC(vpc *infrav1.VPC) error { + if vpc == nil || (vpc.ID == "" && vpc.Name == "") { + return nil + } + + // If VPC ID is provided, check if it exists + if vpc.ID != "" { + resp, count, err := c.cs.VPC.GetVPCByID(vpc.ID, cloudstack.WithProject(c.user.Project.ID)) + if err != nil { + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + return errors.Wrapf(err, "failed to get VPC with ID %s", vpc.ID) + } + if count == 0 { + return errors.Errorf("no VPC found with ID %s", vpc.ID) + } + vpc.Name = resp.Name + return nil + } + + // If VPC name is provided, check if it exists + resp, count, err := c.cs.VPC.GetVPCByName(vpc.Name, cloudstack.WithProject(c.user.Project.ID)) + if err != nil { + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + return errors.Wrapf(err, "failed to get VPC with name %s", vpc.Name) + } + if count == 0 { + return errors.Errorf("no VPC found with name %s", vpc.Name) + } + vpc.ID = resp.Id + return nil +} + +// GetOrCreateVPC ensures a VPC exists for the given specification. +// If the VPC doesn't exist, it creates a new one. +func (c *client) GetOrCreateVPC(vpc *infrav1.VPC) error { + if vpc == nil || (vpc.ID == "" && vpc.Name == "") { + return nil + } + + // Try to resolve the VPC + err := c.ResolveVPC(vpc) + if err != nil { + // If it's a "not found" error and we have a name, create the VPC + if strings.Contains(err.Error(), "no VPC found") && vpc.Name != "" { + return c.CreateVPC(vpc) + } + return err + } + + return nil +} + +// CreateVPC creates a new VPC in CloudStack. +func (c *client) CreateVPC(vpc *infrav1.VPC) error { + if vpc == nil || vpc.Name == "" { + return errors.New("VPC name must be specified") + } + + // Get VPC offering ID + p := c.cs.VPC.NewListVPCOfferingsParams() + resp, err := c.cs.VPC.ListVPCOfferings(p) + if err != nil { + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + return errors.Wrap(err, "failed to list VPC offerings") + } + if resp.Count == 0 { + return errors.New("no VPC offerings available") + } + + // Since the SDK's VPC creation API might have compatibility issues with different CloudStack versions, + // we'll need to handle this in the implementation of the network creation rather than here. + // For now, we'll just return a "not implemented" error, and handle VPC creation in the isolated network creation. + return errors.New("creating VPC not directly implemented; handled in isolated network creation") +} From d7c9476359a21982cce57d671e42ce8ea6257349 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Fri, 7 Mar 2025 14:34:24 +0530 Subject: [PATCH 2/4] Create VPC if it doesn't exit --- api/v1beta1/zz_generated.conversion.go | 6 ++ api/v1beta2/zz_generated.conversion.go | 32 +++++++- api/v1beta3/cloudstackfailuredomain_types.go | 14 +++- .../cloudstackisolatednetwork_types.go | 21 +++-- api/v1beta3/zz_generated.deepcopy.go | 24 ++++-- ...e.cluster.x-k8s.io_cloudstackclusters.yaml | 11 +++ ...ter.x-k8s.io_cloudstackfailuredomains.yaml | 11 +++ ...r.x-k8s.io_cloudstackisolatednetworks.yaml | 9 ++ .../cloudstackfailuredomain_controller.go | 2 +- controllers/utils/isolated_network.go | 8 +- pkg/cloud/isolated_network.go | 82 ++++++++----------- pkg/cloud/network.go | 11 +++ pkg/cloud/vpc.go | 63 ++++++-------- 13 files changed, 190 insertions(+), 104 deletions(-) diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 16c82c57..648ed174 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -506,6 +506,9 @@ func autoConvert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta1_CloudStackIsol out.ID = in.ID out.ControlPlaneEndpoint = in.ControlPlaneEndpoint // WARNING: in.FailureDomainName requires manual conversion: does not exist in peer-type + // WARNING: in.Gateway requires manual conversion: does not exist in peer-type + // WARNING: in.Netmask requires manual conversion: does not exist in peer-type + // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } @@ -987,6 +990,9 @@ func autoConvert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Ne out.ID = in.ID out.Type = in.Type out.Name = in.Name + // WARNING: in.Gateway requires manual conversion: does not exist in peer-type + // WARNING: in.Netmask requires manual conversion: does not exist in peer-type + // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/zz_generated.conversion.go b/api/v1beta2/zz_generated.conversion.go index 9573ab25..cb0f4cdf 100644 --- a/api/v1beta2/zz_generated.conversion.go +++ b/api/v1beta2/zz_generated.conversion.go @@ -757,7 +757,17 @@ func Convert_v1beta3_CloudStackIsolatedNetwork_To_v1beta2_CloudStackIsolatedNetw func autoConvert_v1beta2_CloudStackIsolatedNetworkList_To_v1beta3_CloudStackIsolatedNetworkList(in *CloudStackIsolatedNetworkList, out *v1beta3.CloudStackIsolatedNetworkList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]v1beta3.CloudStackIsolatedNetwork)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]v1beta3.CloudStackIsolatedNetwork, len(*in)) + for i := range *in { + if err := Convert_v1beta2_CloudStackIsolatedNetwork_To_v1beta3_CloudStackIsolatedNetwork(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -768,7 +778,17 @@ func Convert_v1beta2_CloudStackIsolatedNetworkList_To_v1beta3_CloudStackIsolated func autoConvert_v1beta3_CloudStackIsolatedNetworkList_To_v1beta2_CloudStackIsolatedNetworkList(in *v1beta3.CloudStackIsolatedNetworkList, out *CloudStackIsolatedNetworkList, s conversion.Scope) error { out.ListMeta = in.ListMeta - out.Items = *(*[]CloudStackIsolatedNetwork)(unsafe.Pointer(&in.Items)) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CloudStackIsolatedNetwork, len(*in)) + for i := range *in { + if err := Convert_v1beta3_CloudStackIsolatedNetwork_To_v1beta2_CloudStackIsolatedNetwork(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } return nil } @@ -795,6 +815,9 @@ func autoConvert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsol out.ID = in.ID out.ControlPlaneEndpoint = in.ControlPlaneEndpoint out.FailureDomainName = in.FailureDomainName + // WARNING: in.Gateway requires manual conversion: does not exist in peer-type + // WARNING: in.Netmask requires manual conversion: does not exist in peer-type + // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } @@ -1271,10 +1294,13 @@ func autoConvert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Ne out.ID = in.ID out.Type = in.Type out.Name = in.Name + // WARNING: in.Gateway requires manual conversion: does not exist in peer-type + // WARNING: in.Netmask requires manual conversion: does not exist in peer-type + // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta3_Network_To_v1beta2_Network is an autogenerated conversion function. +// Convert_v1beta3_Network_To_v1beta1_Network is an autogenerated conversion function. func Convert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Network, s conversion.Scope) error { return autoConvert_v1beta3_Network_To_v1beta2_Network(in, out, s) } diff --git a/api/v1beta3/cloudstackfailuredomain_types.go b/api/v1beta3/cloudstackfailuredomain_types.go index 40a93c15..4f3f2cda 100644 --- a/api/v1beta3/cloudstackfailuredomain_types.go +++ b/api/v1beta3/cloudstackfailuredomain_types.go @@ -54,9 +54,17 @@ type Network struct { // Cloudstack Network Name the cluster is built in. Name string `json:"name"` + // Cloudstack Network Gateway the cluster is built in. + // +optional + Gateway string `json:"gateway,omitempty"` + + // Cloudstack Network Netmask the cluster is built in. + // +optional + Netmask string `json:"netmask,omitempty"` + // Cloudstack VPC the network belongs to. // +optional - VPC VPC `json:"vpc,omitempty"` + VPC *VPC `json:"vpc,omitempty"` } type VPC struct { @@ -67,6 +75,10 @@ type VPC struct { // Cloudstack VPC Name of the network. // +optional Name string `json:"name"` + + // CIDR for the VPC. + // +optional + CIDR string `json:"cidr,omitempty"` } // CloudStackZoneSpec specifies a Zone's details. diff --git a/api/v1beta3/cloudstackisolatednetwork_types.go b/api/v1beta3/cloudstackisolatednetwork_types.go index dddd3565..76b2a1f5 100644 --- a/api/v1beta3/cloudstackisolatednetwork_types.go +++ b/api/v1beta3/cloudstackisolatednetwork_types.go @@ -40,9 +40,17 @@ type CloudStackIsolatedNetworkSpec struct { // FailureDomainName -- the FailureDomain the network is placed in. FailureDomainName string `json:"failureDomainName"` + // Gateway for the network. + // +optional + Gateway string `json:"gateway,omitempty"` + + // Netmask for the network. + // +optional + Netmask string `json:"netmask,omitempty"` + // VPC the network belongs to. // +optional - VPC VPC `json:"vpc,omitempty"` + VPC *VPC `json:"vpc,omitempty"` } // CloudStackIsolatedNetworkStatus defines the observed state of CloudStackIsolatedNetwork @@ -59,10 +67,13 @@ type CloudStackIsolatedNetworkStatus struct { func (n *CloudStackIsolatedNetwork) Network() *Network { return &Network{ - Name: n.Spec.Name, - Type: "IsolatedNetwork", - ID: n.Spec.ID, - VPC: n.Spec.VPC} + Name: n.Spec.Name, + Type: "IsolatedNetwork", + ID: n.Spec.ID, + Gateway: n.Spec.Gateway, + Netmask: n.Spec.Netmask, + VPC: n.Spec.VPC, + } } //+kubebuilder:object:root=true diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index e495ee1a..81d659d4 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -181,7 +181,9 @@ func (in *CloudStackClusterSpec) DeepCopyInto(out *CloudStackClusterSpec) { if in.FailureDomains != nil { in, out := &in.FailureDomains, &out.FailureDomains *out = make([]CloudStackFailureDomainSpec, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } out.ControlPlaneEndpoint = in.ControlPlaneEndpoint if in.SyncWithACS != nil { @@ -228,7 +230,7 @@ func (in *CloudStackFailureDomain) DeepCopyInto(out *CloudStackFailureDomain) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -285,7 +287,7 @@ func (in *CloudStackFailureDomainList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudStackFailureDomainSpec) DeepCopyInto(out *CloudStackFailureDomainSpec) { *out = *in - out.Zone = in.Zone + in.Zone.DeepCopyInto(&out.Zone) out.ACSEndpoint = in.ACSEndpoint } @@ -319,7 +321,7 @@ func (in *CloudStackIsolatedNetwork) DeepCopyInto(out *CloudStackIsolatedNetwork *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status } @@ -377,7 +379,11 @@ func (in *CloudStackIsolatedNetworkList) DeepCopyObject() runtime.Object { func (in *CloudStackIsolatedNetworkSpec) DeepCopyInto(out *CloudStackIsolatedNetworkSpec) { *out = *in out.ControlPlaneEndpoint = in.ControlPlaneEndpoint - out.VPC = in.VPC + if in.VPC != nil { + in, out := &in.VPC, &out.VPC + *out = new(VPC) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStackIsolatedNetworkSpec. @@ -759,7 +765,7 @@ func (in *CloudStackResourceIdentifier) DeepCopy() *CloudStackResourceIdentifier // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CloudStackZoneSpec) DeepCopyInto(out *CloudStackZoneSpec) { *out = *in - out.Network = in.Network + in.Network.DeepCopyInto(&out.Network) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudStackZoneSpec. @@ -775,7 +781,11 @@ func (in *CloudStackZoneSpec) DeepCopy() *CloudStackZoneSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Network) DeepCopyInto(out *Network) { *out = *in - out.VPC = in.VPC + if in.VPC != nil { + in, out := &in.VPC, &out.VPC + *out = new(VPC) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml index ed7cbae6..9839d0fe 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml @@ -391,6 +391,10 @@ spec: network: description: The network within the Zone to use. properties: + gateway: + description: Cloudstack Network Gateway the cluster + is built in. + type: string id: description: Cloudstack Network ID the cluster is built in. @@ -399,6 +403,10 @@ spec: description: Cloudstack Network Name the cluster is built in. type: string + netmask: + description: Cloudstack Network Netmask the cluster + is built in. + type: string type: description: Cloudstack Network Type the cluster is built in. @@ -406,6 +414,9 @@ spec: vpc: description: Cloudstack VPC the network belongs to. properties: + cidr: + description: CIDR for the VPC. + type: string id: description: Cloudstack VPC ID of the network. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml index f216d011..936b10f7 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml @@ -169,6 +169,10 @@ spec: network: description: The network within the Zone to use. properties: + gateway: + description: Cloudstack Network Gateway the cluster is built + in. + type: string id: description: Cloudstack Network ID the cluster is built in. type: string @@ -176,6 +180,10 @@ spec: description: Cloudstack Network Name the cluster is built in. type: string + netmask: + description: Cloudstack Network Netmask the cluster is built + in. + type: string type: description: Cloudstack Network Type the cluster is built in. @@ -183,6 +191,9 @@ spec: vpc: description: Cloudstack VPC the network belongs to. properties: + cidr: + description: CIDR for the VPC. + type: string id: description: Cloudstack VPC ID of the network. type: string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml index cc4b3045..9c24ddc1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml @@ -192,15 +192,24 @@ spec: description: FailureDomainName -- the FailureDomain the network is placed in. type: string + gateway: + description: Gateway for the network. + type: string id: description: ID. type: string name: description: Name. type: string + netmask: + description: Netmask for the network. + type: string vpc: description: VPC the network belongs to. properties: + cidr: + description: CIDR for the VPC. + type: string id: description: Cloudstack VPC ID of the network. type: string diff --git a/controllers/cloudstackfailuredomain_controller.go b/controllers/cloudstackfailuredomain_controller.go index 3084cf6e..d3d9e210 100644 --- a/controllers/cloudstackfailuredomain_controller.go +++ b/controllers/cloudstackfailuredomain_controller.go @@ -107,7 +107,7 @@ func (r *CloudStackFailureDomainReconciliationRunner) Reconcile() (retRes ctrl.R r.ReconciliationSubject.Spec.Zone.Network.Type == infrav1.NetworkTypeIsolated { netName := r.ReconciliationSubject.Spec.Zone.Network.Name if res, err := r.GenerateIsolatedNetwork( - netName, func() string { return r.ReconciliationSubject.Spec.Name })(); r.ShouldReturn(res, err) { + netName, func() string { return r.ReconciliationSubject.Spec.Name }, r.ReconciliationSubject.Spec.Zone.Network)(); r.ShouldReturn(res, err) { return res, err } else if res, err := r.GetObjectByName(r.IsoNetMetaName(netName), r.IsoNet)(); r.ShouldReturn(res, err) { return res, err diff --git a/controllers/utils/isolated_network.go b/controllers/utils/isolated_network.go index 05050c85..83e20000 100644 --- a/controllers/utils/isolated_network.go +++ b/controllers/utils/isolated_network.go @@ -33,7 +33,7 @@ func (r *ReconciliationRunner) IsoNetMetaName(name string) string { } // GenerateIsolatedNetwork of the passed name that's owned by the ReconciliationSubject. -func (r *ReconciliationRunner) GenerateIsolatedNetwork(name string, fdNameFunc func() string) CloudStackReconcilerMethod { +func (r *ReconciliationRunner) GenerateIsolatedNetwork(name string, fdNameFunc func() string, network infrav1.Network) CloudStackReconcilerMethod { return func() (ctrl.Result, error) { lowerName := strings.ToLower(name) metaName := r.IsoNetMetaName(lowerName) @@ -43,6 +43,12 @@ func (r *ReconciliationRunner) GenerateIsolatedNetwork(name string, fdNameFunc f csIsoNet.Spec.FailureDomainName = fdNameFunc() csIsoNet.Spec.ControlPlaneEndpoint.Host = r.CSCluster.Spec.ControlPlaneEndpoint.Host csIsoNet.Spec.ControlPlaneEndpoint.Port = r.CSCluster.Spec.ControlPlaneEndpoint.Port + csIsoNet.Spec.Gateway = network.Gateway + csIsoNet.Spec.Netmask = network.Netmask + + if network.VPC != nil { + csIsoNet.Spec.VPC = network.VPC + } if err := r.K8sClient.Create(r.RequestCtx, csIsoNet); err != nil && !ContainsAlreadyExistsSubstring(err) { return r.ReturnWrappedError(err, "creating isolated network CRD") diff --git a/pkg/cloud/isolated_network.go b/pkg/cloud/isolated_network.go index 5ff89144..431b0279 100644 --- a/pkg/cloud/isolated_network.go +++ b/pkg/cloud/isolated_network.go @@ -41,8 +41,8 @@ type IsoNetworkIface interface { } // getOfferingID fetches an offering id. -func (c *client) getOfferingID() (string, error) { - offeringID, count, retErr := c.cs.NetworkOffering.GetNetworkOfferingID(NetOffering) +func (c *client) getOfferingID(offeringName string) (string, error) { + offeringID, count, retErr := c.cs.NetworkOffering.GetNetworkOfferingID(offeringName) if retErr != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(retErr) return "", retErr @@ -68,7 +68,7 @@ func (c *client) AssociatePublicIPAddress( isoNet.Status.PublicIPID = publicAddress.Id // Check if the address is already associated with the network or VPC. - if publicAddress.Associatednetworkid == isoNet.Spec.ID || publicAddress.Vpcid == isoNet.Spec.VPC.ID { + if publicAddress.Associatednetworkid == isoNet.Spec.ID || (isoNet.Spec.VPC != nil && publicAddress.Vpcid == isoNet.Spec.VPC.ID) { return nil } @@ -76,7 +76,7 @@ func (c *client) AssociatePublicIPAddress( p := c.cs.Address.NewAssociateIpAddressParams() p.SetIpaddress(isoNet.Spec.ControlPlaneEndpoint.Host) p.SetNetworkid(isoNet.Spec.ID) - if isoNet.Spec.VPC.ID != "" { + if isoNet.Spec.VPC != nil && isoNet.Spec.VPC.ID != "" { p.SetVpcid(isoNet.Spec.VPC.ID) } setIfNotEmpty(c.user.Project.ID, p.SetProjectid) @@ -98,63 +98,42 @@ func (c *client) AssociatePublicIPAddress( // CreateIsolatedNetwork creates an isolated network in the relevant FailureDomain per passed network specification. func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoNet *infrav1.CloudStackIsolatedNetwork) (retErr error) { // Get network offering ID. - offeringID, err := c.getOfferingID() - if err != nil { - return err - } - + offeringName := NetOffering // First, check if VPC is specified and handle it - if isoNet.Spec.VPC.Name != "" || isoNet.Spec.VPC.ID != "" { + if isoNet.Spec.VPC != nil && (isoNet.Spec.VPC.Name != "" || isoNet.Spec.VPC.ID != "") { // Try to resolve or create the VPC - err := c.ResolveVPC(&isoNet.Spec.VPC) - if err != nil { - return errors.Wrap(err, "resolving VPC for isolated network") - // } - - // TODO: Handle VPC creation - - // // VPC not found, need to create it - // // First, get a VPC offering ID - // vpcOfferingParams := c.cs.VPC.NewListVPCOfferingsParams() - // vpcOfferingResp, err := c.cs.VPC.ListVPCOfferings(vpcOfferingParams) - // if err != nil { - // c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) - // return errors.Wrap(err, "listing VPC offerings") - // } - // if vpcOfferingResp.Count == 0 { - // return errors.New("no VPC offerings available") - // } - - // // Use the first VPC offering - // vpcOfferingID := vpcOfferingResp.VPCOfferings[0].Id - - // // Create the VPC - // vpcParams := c.cs.VPC.NewCreateVPCParams(isoNet.Spec.VPC.Name, vpcOfferingID, fd.Spec.Zone.ID) - // vpcParams.SetDisplaytext(isoNet.Spec.VPC.Name) - // setIfNotEmpty(c.user.Project.ID, vpcParams.SetProjectid) - - // vpcResp, err := c.cs.VPC.CreateVPC(vpcParams) - // if err != nil { - // c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) - // return errors.Wrapf(err, "creating VPC with name %s", isoNet.Spec.VPC.Name) - // } - - // isoNet.Spec.VPC.ID = vpcResp.Id - - // // Tag the VPC - // if err := c.AddCreatedByCAPCTag(ResourceTypeVPC, isoNet.Spec.VPC.ID); err != nil { - // return errors.Wrapf(err, "tagging VPC with ID %s", isoNet.Spec.VPC.ID) - // } + err := c.ResolveVPC(isoNet.Spec.VPC) + if err != nil { // No VPC found, create it + err = c.CreateVPC(fd, isoNet.Spec.VPC) + if err != nil { + return errors.Wrap(err, "creating VPC with name "+isoNet.Spec.VPC.Name) + } } + offeringName = NetVPCOffering + } + + // Get network offering ID. + offeringID, err := c.getOfferingID(offeringName) + if err != nil { + return err } // Do isolated network creation. p := c.cs.Network.NewCreateNetworkParams(isoNet.Spec.Name, offeringID, fd.Spec.Zone.ID) p.SetDisplaytext(isoNet.Spec.Name) + + if isoNet.Spec.Gateway != "" { + p.SetGateway(isoNet.Spec.Gateway) + } + + if isoNet.Spec.Netmask != "" { + p.SetNetmask(isoNet.Spec.Netmask) + } + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) // If VPC is specified, set the VPC ID for the network - if isoNet.Spec.VPC.ID != "" { + if isoNet.Spec.VPC != nil && isoNet.Spec.VPC.ID != "" { p.SetVpcid(isoNet.Spec.VPC.ID) } @@ -169,6 +148,9 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN // OpenFirewallRules opens a CloudStack egress firewall for an isolated network. func (c *client) OpenFirewallRules(isoNet *infrav1.CloudStackIsolatedNetwork) (retErr error) { + if isoNet.Spec.VPC != nil && isoNet.Spec.VPC.ID != "" { + return nil + } protocols := []string{NetworkProtocolTCP, NetworkProtocolUDP, NetworkProtocolICMP} for _, proto := range protocols { p := c.cs.Firewall.NewCreateEgressFirewallRuleParams(isoNet.Spec.ID, proto) diff --git a/pkg/cloud/network.go b/pkg/cloud/network.go index 053299fc..ec29203c 100644 --- a/pkg/cloud/network.go +++ b/pkg/cloud/network.go @@ -30,6 +30,7 @@ type NetworkIface interface { const ( NetOffering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + NetVPCOffering = "DefaultIsolatedNetworkOfferingForVpcNetworks" K8sDefaultAPIPort = 6443 NetworkTypeIsolated = "Isolated" NetworkTypeShared = "Shared" @@ -62,7 +63,12 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { } else { // Got netID from the network's name. net.ID = netDetails.Id net.Type = netDetails.Type + net.Gateway = netDetails.Gateway + net.Netmask = netDetails.Netmask if netDetails.Vpcid != "" { + if net.VPC == nil { + net.VPC = &infrav1.VPC{} + } net.VPC.ID = netDetails.Vpcid net.VPC.Name = netDetails.Vpcname } @@ -80,7 +86,12 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { net.Name = netDetails.Name net.ID = netDetails.Id net.Type = netDetails.Type + net.Gateway = netDetails.Gateway + net.Netmask = netDetails.Netmask if netDetails.Vpcid != "" { + if net.VPC == nil { + net.VPC = &infrav1.VPC{} + } net.VPC.ID = netDetails.Vpcid net.VPC.Name = netDetails.Vpcname } diff --git a/pkg/cloud/vpc.go b/pkg/cloud/vpc.go index 14c732fc..496eff04 100644 --- a/pkg/cloud/vpc.go +++ b/pkg/cloud/vpc.go @@ -17,8 +17,6 @@ limitations under the License. package cloud import ( - "strings" - infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" "github.com/apache/cloudstack-go/v2/cloudstack" @@ -26,13 +24,27 @@ import ( ) // ResourceTypeVPC is the type identifier for VPC resources. -const ResourceTypeVPC = "VPC" +const ( + ResourceTypeVPC = "VPC" + VPCOffering = "Default VPC offering" +) // VPCIface defines the interface for VPC operations. type VPCIface interface { ResolveVPC(*infrav1.VPC) error - CreateVPC(*infrav1.VPC) error - GetOrCreateVPC(*infrav1.VPC) error + CreateVPC(*infrav1.CloudStackFailureDomain, *infrav1.VPC) error +} + +// getVPCOfferingID fetches a vpc offering id. +func (c *client) getVPCOfferingID() (string, error) { + offeringID, count, retErr := c.cs.VPC.GetVPCOfferingID(VPCOffering) + if retErr != nil { + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(retErr) + return "", retErr + } else if count != 1 { + return "", errors.New("found more than one vpc offering") + } + return offeringID, nil } // ResolveVPC checks if the specified VPC exists by ID or name. @@ -69,45 +81,24 @@ func (c *client) ResolveVPC(vpc *infrav1.VPC) error { return nil } -// GetOrCreateVPC ensures a VPC exists for the given specification. -// If the VPC doesn't exist, it creates a new one. -func (c *client) GetOrCreateVPC(vpc *infrav1.VPC) error { - if vpc == nil || (vpc.ID == "" && vpc.Name == "") { - return nil +// CreateVPC creates a new VPC in CloudStack. +func (c *client) CreateVPC(fd *infrav1.CloudStackFailureDomain, vpc *infrav1.VPC) error { + if vpc == nil || vpc.Name == "" { + return errors.New("VPC name must be specified") } - // Try to resolve the VPC - err := c.ResolveVPC(vpc) + offeringID, err := c.getVPCOfferingID() if err != nil { - // If it's a "not found" error and we have a name, create the VPC - if strings.Contains(err.Error(), "no VPC found") && vpc.Name != "" { - return c.CreateVPC(vpc) - } return err } - return nil -} + p := c.cs.VPC.NewCreateVPCParams(vpc.CIDR, vpc.Name, vpc.Name, offeringID, fd.Spec.Zone.ID) -// CreateVPC creates a new VPC in CloudStack. -func (c *client) CreateVPC(vpc *infrav1.VPC) error { - if vpc == nil || vpc.Name == "" { - return errors.New("VPC name must be specified") - } - - // Get VPC offering ID - p := c.cs.VPC.NewListVPCOfferingsParams() - resp, err := c.cs.VPC.ListVPCOfferings(p) + resp, err := c.cs.VPC.CreateVPC(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) - return errors.Wrap(err, "failed to list VPC offerings") - } - if resp.Count == 0 { - return errors.New("no VPC offerings available") + return errors.Wrapf(err, "creating VPC with name %s", vpc.Name) } - - // Since the SDK's VPC creation API might have compatibility issues with different CloudStack versions, - // we'll need to handle this in the implementation of the network creation rather than here. - // For now, we'll just return a "not implemented" error, and handle VPC creation in the isolated network creation. - return errors.New("creating VPC not directly implemented; handled in isolated network creation") + vpc.ID = resp.Id + return nil } From 6c03ddedae8a7cc340296b3a61a01c837bab9fc0 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Fri, 7 Mar 2025 18:12:09 +0530 Subject: [PATCH 3/4] Add tests for vpc --- api/v1beta1/conversion.go | 11 + api/v1beta1/zz_generated.conversion.go | 15 +- api/v1beta2/conversion.go | 28 +++ api/v1beta2/zz_generated.conversion.go | 30 +-- .../cloudstackfailuredomain_controller.go | 4 +- pkg/cloud/isolated_network.go | 53 ++--- pkg/cloud/vpc.go | 60 ++++- pkg/cloud/vpc_test.go | 224 ++++++++++++++++++ test/e2e/common.go | 34 ++- test/e2e/config/cloudstack.yaml | 10 +- .../cloudstack-cluster.yaml | 7 +- .../v1beta3/cluster-template-project/md.yaml | 4 +- .../cluster-with-vpc-network.yaml | 23 ++ .../kustomization.yaml | 6 + test/e2e/project.go | 39 ++- test/e2e/vpc_network.go | 96 ++++++++ test/e2e/vpc_network_test.go | 40 ++++ 17 files changed, 610 insertions(+), 74 deletions(-) create mode 100644 pkg/cloud/vpc_test.go create mode 100644 test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/cluster-with-vpc-network.yaml create mode 100644 test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/kustomization.yaml create mode 100644 test/e2e/vpc_network.go create mode 100644 test/e2e/vpc_network_test.go diff --git a/api/v1beta1/conversion.go b/api/v1beta1/conversion.go index aad7c9d1..05086cc0 100644 --- a/api/v1beta1/conversion.go +++ b/api/v1beta1/conversion.go @@ -174,3 +174,14 @@ func GetK8sSecret(name, namespace string) (*corev1.Secret, error) { } return endpointCredentials, nil } + +// Convert_v1beta3_Network_To_v1beta1_Network converts from v1beta3.Network to v1beta1.Network +// +//nolint:golint,revive,stylecheck +func Convert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Network, _ conv.Scope) error { + out.ID = in.ID + out.Type = in.Type + out.Name = in.Name + // Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta1.Network + return nil +} diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 648ed174..8a52cde5 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -243,11 +243,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta3.Network)(nil), (*Network)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta3_Network_To_v1beta1_Network(a.(*v1beta3.Network), b.(*Network), scope) - }); err != nil { - return err - } if err := s.AddConversionFunc((*v1.ObjectMeta)(nil), (*apiv1beta1.ObjectMeta)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_ObjectMeta_To_v1beta1_ObjectMeta(a.(*v1.ObjectMeta), b.(*apiv1beta1.ObjectMeta), scope) }); err != nil { @@ -298,6 +293,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta3.Network)(nil), (*Network)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta3_Network_To_v1beta1_Network(a.(*v1beta3.Network), b.(*Network), scope) + }); err != nil { + return err + } return nil } @@ -995,8 +995,3 @@ func autoConvert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Ne // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } - -// Convert_v1beta3_Network_To_v1beta1_Network is an autogenerated conversion function. -func Convert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Network, s conversion.Scope) error { - return autoConvert_v1beta3_Network_To_v1beta1_Network(in, out, s) -} diff --git a/api/v1beta2/conversion.go b/api/v1beta2/conversion.go index 8caed4ea..47cc758c 100644 --- a/api/v1beta2/conversion.go +++ b/api/v1beta2/conversion.go @@ -15,3 +15,31 @@ limitations under the License. */ package v1beta2 + +import ( + conv "k8s.io/apimachinery/pkg/conversion" + "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" +) + +// Convert_v1beta3_Network_To_v1beta2_Network converts from v1beta3.Network to v1beta2.Network +// +//nolint:golint,revive,stylecheck +func Convert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Network, _ conv.Scope) error { + out.ID = in.ID + out.Type = in.Type + out.Name = in.Name + // Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta2.Network + return nil +} + +// Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec converts from v1beta3.CloudStackIsolatedNetworkSpec to v1beta2.CloudStackIsolatedNetworkSpec +// +//nolint:golint,revive,stylecheck +func Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(in *v1beta3.CloudStackIsolatedNetworkSpec, out *CloudStackIsolatedNetworkSpec, _ conv.Scope) error { + out.Name = in.Name + out.ID = in.ID + out.ControlPlaneEndpoint = in.ControlPlaneEndpoint + out.FailureDomainName = in.FailureDomainName + // Skip Gateway, Netmask, and VPC fields as they do not exist in v1beta2.CloudStackIsolatedNetworkSpec + return nil +} diff --git a/api/v1beta2/zz_generated.conversion.go b/api/v1beta2/zz_generated.conversion.go index cb0f4cdf..0619f467 100644 --- a/api/v1beta2/zz_generated.conversion.go +++ b/api/v1beta2/zz_generated.conversion.go @@ -168,11 +168,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta3.CloudStackIsolatedNetworkSpec)(nil), (*CloudStackIsolatedNetworkSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(a.(*v1beta3.CloudStackIsolatedNetworkSpec), b.(*CloudStackIsolatedNetworkSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*CloudStackIsolatedNetworkStatus)(nil), (*v1beta3.CloudStackIsolatedNetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_CloudStackIsolatedNetworkStatus_To_v1beta3_CloudStackIsolatedNetworkStatus(a.(*CloudStackIsolatedNetworkStatus), b.(*v1beta3.CloudStackIsolatedNetworkStatus), scope) }); err != nil { @@ -328,11 +323,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta3.Network)(nil), (*Network)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta3_Network_To_v1beta2_Network(a.(*v1beta3.Network), b.(*Network), scope) - }); err != nil { - return err - } if err := s.AddConversionFunc((*v1.ObjectMeta)(nil), (*v1beta1.ObjectMeta)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_ObjectMeta_To_v1beta1_ObjectMeta(a.(*v1.ObjectMeta), b.(*v1beta1.ObjectMeta), scope) }); err != nil { @@ -363,11 +353,21 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta3.CloudStackIsolatedNetworkSpec)(nil), (*CloudStackIsolatedNetworkSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(a.(*v1beta3.CloudStackIsolatedNetworkSpec), b.(*CloudStackIsolatedNetworkSpec), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta3.CloudStackMachineTemplateSpec)(nil), (*CloudStackMachineTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta3_CloudStackMachineTemplateSpec_To_v1beta2_CloudStackMachineTemplateSpec(a.(*v1beta3.CloudStackMachineTemplateSpec), b.(*CloudStackMachineTemplateSpec), scope) }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta3.Network)(nil), (*Network)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta3_Network_To_v1beta2_Network(a.(*v1beta3.Network), b.(*Network), scope) + }); err != nil { + return err + } return nil } @@ -821,11 +821,6 @@ func autoConvert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsol return nil } -// Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec is an autogenerated conversion function. -func Convert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(in *v1beta3.CloudStackIsolatedNetworkSpec, out *CloudStackIsolatedNetworkSpec, s conversion.Scope) error { - return autoConvert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsolatedNetworkSpec(in, out, s) -} - func autoConvert_v1beta2_CloudStackIsolatedNetworkStatus_To_v1beta3_CloudStackIsolatedNetworkStatus(in *CloudStackIsolatedNetworkStatus, out *v1beta3.CloudStackIsolatedNetworkStatus, s conversion.Scope) error { out.PublicIPID = in.PublicIPID out.LBRuleID = in.LBRuleID @@ -1299,8 +1294,3 @@ func autoConvert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Ne // WARNING: in.VPC requires manual conversion: does not exist in peer-type return nil } - -// Convert_v1beta3_Network_To_v1beta1_Network is an autogenerated conversion function. -func Convert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Network, s conversion.Scope) error { - return autoConvert_v1beta3_Network_To_v1beta2_Network(in, out, s) -} diff --git a/controllers/cloudstackfailuredomain_controller.go b/controllers/cloudstackfailuredomain_controller.go index d3d9e210..4dbb04cc 100644 --- a/controllers/cloudstackfailuredomain_controller.go +++ b/controllers/cloudstackfailuredomain_controller.go @@ -107,7 +107,9 @@ func (r *CloudStackFailureDomainReconciliationRunner) Reconcile() (retRes ctrl.R r.ReconciliationSubject.Spec.Zone.Network.Type == infrav1.NetworkTypeIsolated { netName := r.ReconciliationSubject.Spec.Zone.Network.Name if res, err := r.GenerateIsolatedNetwork( - netName, func() string { return r.ReconciliationSubject.Spec.Name }, r.ReconciliationSubject.Spec.Zone.Network)(); r.ShouldReturn(res, err) { + netName, + func() string { return r.ReconciliationSubject.Spec.Name }, + r.ReconciliationSubject.Spec.Zone.Network)(); r.ShouldReturn(res, err) { return res, err } else if res, err := r.GetObjectByName(r.IsoNetMetaName(netName), r.IsoNet)(); r.ShouldReturn(res, err) { return res, err diff --git a/pkg/cloud/isolated_network.go b/pkg/cloud/isolated_network.go index 431b0279..8d82a815 100644 --- a/pkg/cloud/isolated_network.go +++ b/pkg/cloud/isolated_network.go @@ -143,6 +143,8 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN return errors.Wrapf(err, "creating network with name %s", isoNet.Spec.Name) } isoNet.Spec.ID = resp.Id + isoNet.Spec.Gateway = resp.Gateway + isoNet.Spec.Netmask = resp.Netmask return c.AddCreatedByCAPCTag(ResourceTypeNetwork, isoNet.Spec.ID) } @@ -204,31 +206,6 @@ func (c *client) GetPublicIP( return nil, errors.New("no public addresses found in available networks") } -// GetIsolatedNetwork gets an isolated network in the relevant Zone. -func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) (retErr error) { - netDetails, count, err := c.cs.Network.GetNetworkByName(isoNet.Spec.Name, cloudstack.WithProject(c.user.Project.ID)) - if err != nil { - c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) - retErr = multierror.Append(retErr, errors.Wrapf(err, "could not get Network ID from %s", isoNet.Spec.Name)) - } else if count != 1 { - retErr = multierror.Append(retErr, errors.Errorf( - "expected 1 Network with name %s, but got %d", isoNet.Name, count)) - } else { // Got netID from the network's name. - isoNet.Spec.ID = netDetails.Id - return nil - } - - netDetails, count, err = c.cs.Network.GetNetworkByID(isoNet.Spec.ID, cloudstack.WithProject(c.user.Project.ID)) - if err != nil { - c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) - return multierror.Append(retErr, errors.Wrapf(err, "could not get Network by ID %s", isoNet.Spec.ID)) - } else if count != 1 { - return multierror.Append(retErr, errors.Errorf("expected 1 Network with UUID %s, but got %d", isoNet.Spec.ID, count)) - } - isoNet.Name = netDetails.Name - return nil -} - // ResolveLoadBalancerRuleDetails resolves the details of a load balancer rule by PublicIPID and Port. func (c *client) ResolveLoadBalancerRuleDetails( isoNet *infrav1.CloudStackIsolatedNetwork, @@ -303,6 +280,11 @@ func (c *client) GetOrCreateIsolatedNetwork( } } else { // Network existed and was resolved. Set ID on isoNet CloudStackIsolatedNetwork in case it only had name set. isoNet.Spec.ID = net.ID + isoNet.Spec.Gateway = net.Gateway + isoNet.Spec.Netmask = net.Netmask + if net.VPC != nil && net.VPC.ID != "" { + isoNet.Spec.VPC = net.VPC + } } // Tag the created network. @@ -311,6 +293,13 @@ func (c *client) GetOrCreateIsolatedNetwork( return errors.Wrapf(err, "tagging network with id %s", networkID) } + // Tag the created VPC. + if net.VPC != nil && net.VPC.ID != "" { + if err := c.AddClusterTag(ResourceTypeVPC, net.VPC.ID, csCluster); err != nil { + return errors.Wrapf(err, "tagging VPC with id %s", net.VPC.ID) + } + } + // Associate Public IP with CloudStackIsolatedNetwork if err := c.AssociatePublicIPAddress(fd, isoNet, csCluster); err != nil { return errors.Wrapf(err, "associating public IP address to csCluster") @@ -372,8 +361,18 @@ func (c *client) DisposeIsoNetResources( if err := c.RemoveClusterTagFromNetwork(csCluster, *isoNet.Network()); err != nil { return err } - err := c.DeleteNetworkIfNotInUse(*isoNet.Network()) - return err + if err := c.DeleteNetworkIfNotInUse(*isoNet.Network()); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no match found") { + return err + } + if isoNet.Spec.VPC != nil && isoNet.Spec.VPC.ID != "" { + if err := c.RemoveClusterTagFromVPC(csCluster, *isoNet.Spec.VPC); err != nil { + return err + } + if err := c.DeleteVPCIfNotInUse(*isoNet.Spec.VPC); err != nil && !strings.Contains(strings.ToLower(err.Error()), "no match found") { + return err + } + } + return nil } // DeleteNetworkIfNotInUse deletes an isolated network if the network is no longer in use (indicated by in use tags). diff --git a/pkg/cloud/vpc.go b/pkg/cloud/vpc.go index 496eff04..65ba370a 100644 --- a/pkg/cloud/vpc.go +++ b/pkg/cloud/vpc.go @@ -17,6 +17,8 @@ limitations under the License. package cloud import ( + "strings" + infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" "github.com/apache/cloudstack-go/v2/cloudstack" @@ -25,7 +27,7 @@ import ( // ResourceTypeVPC is the type identifier for VPC resources. const ( - ResourceTypeVPC = "VPC" + ResourceTypeVPC = "Vpc" VPCOffering = "Default VPC offering" ) @@ -33,6 +35,8 @@ const ( type VPCIface interface { ResolveVPC(*infrav1.VPC) error CreateVPC(*infrav1.CloudStackFailureDomain, *infrav1.VPC) error + RemoveClusterTagFromVPC(*infrav1.CloudStackCluster, infrav1.VPC) error + DeleteVPCIfNotInUse(infrav1.VPC) (retError error) } // getVPCOfferingID fetches a vpc offering id. @@ -65,6 +69,7 @@ func (c *client) ResolveVPC(vpc *infrav1.VPC) error { return errors.Errorf("no VPC found with ID %s", vpc.ID) } vpc.Name = resp.Name + vpc.CIDR = resp.Cidr return nil } @@ -78,6 +83,7 @@ func (c *client) ResolveVPC(vpc *infrav1.VPC) error { return errors.Errorf("no VPC found with name %s", vpc.Name) } vpc.ID = resp.Id + vpc.CIDR = resp.Cidr return nil } @@ -93,12 +99,62 @@ func (c *client) CreateVPC(fd *infrav1.CloudStackFailureDomain, vpc *infrav1.VPC } p := c.cs.VPC.NewCreateVPCParams(vpc.CIDR, vpc.Name, vpc.Name, offeringID, fd.Spec.Zone.ID) - + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) + p.SetStart(true) resp, err := c.cs.VPC.CreateVPC(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) return errors.Wrapf(err, "creating VPC with name %s", vpc.Name) } vpc.ID = resp.Id + return c.AddCreatedByCAPCTag(ResourceTypeVPC, vpc.ID) +} + +// DeleteVPC deletes a VPC. +func (c *client) DeleteVPC(vpc infrav1.VPC) error { + _, err := c.cs.VPC.DeleteVPC(c.cs.VPC.NewDeleteVPCParams(vpc.ID)) + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + return errors.Wrapf(err, "deleting vpc with id %s", vpc.ID) +} + +// DeleteVPCIfNotInUse deletes a VPC if the VPC is no longer in use (indicated by in use tags). +func (c *client) DeleteVPCIfNotInUse(vpc infrav1.VPC) (retError error) { + tags, err := c.GetTags(ResourceTypeVPC, vpc.ID) + if err != nil { + return err + } + + var clusterTagCount int + for tagName := range tags { + if strings.HasPrefix(tagName, ClusterTagNamePrefix) { + clusterTagCount++ + } + } + + if clusterTagCount == 0 && tags[CreatedByCAPCTagName] != "" { + return c.DeleteVPC(vpc) + } + + return nil +} + +func generateVPCTagName(csCluster *infrav1.CloudStackCluster) string { + return ClusterTagNamePrefix + string(csCluster.UID) +} + +// RemoveClusterTagFromVPC removes the cluster in use tag from a VPC. +func (c *client) RemoveClusterTagFromVPC(csCluster *infrav1.CloudStackCluster, vpc infrav1.VPC) (retError error) { + tags, err := c.GetTags(ResourceTypeVPC, vpc.ID) + if err != nil { + return err + } + + ClusterTagName := generateVPCTagName(csCluster) + if tagValue := tags[ClusterTagName]; tagValue != "" { + if err = c.DeleteTags(ResourceTypeVPC, vpc.ID, map[string]string{ClusterTagName: tagValue}); err != nil { + return err + } + } + return nil } diff --git a/pkg/cloud/vpc_test.go b/pkg/cloud/vpc_test.go new file mode 100644 index 00000000..6ede0cc5 --- /dev/null +++ b/pkg/cloud/vpc_test.go @@ -0,0 +1,224 @@ +/* +Copyright 2024 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 cloud_test + +import ( + "errors" + "fmt" + + csapi "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/golang/mock/gomock" + "github.com/onsi/ginkgo/v2" + gomega "github.com/onsi/gomega" + infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" + "sigs.k8s.io/cluster-api-provider-cloudstack/pkg/cloud" + dummies "sigs.k8s.io/cluster-api-provider-cloudstack/test/dummies/v1beta3" +) + +var _ = ginkgo.Describe("VPC", func() { + var ( + mockCtrl *gomock.Controller + mockClient *csapi.CloudStackClient + vs *csapi.MockVPCServiceIface + rs *csapi.MockResourcetagsServiceIface + client cloud.Client + ) + + ginkgo.BeforeEach(func() { + // Setup new mock services. + mockCtrl = gomock.NewController(ginkgo.GinkgoT()) + mockClient = csapi.NewMockClient(mockCtrl) + vs = mockClient.VPC.(*csapi.MockVPCServiceIface) + rs = mockClient.Resourcetags.(*csapi.MockResourcetagsServiceIface) + client = cloud.NewClientFromCSAPIClient(mockClient, nil) + dummies.SetDummyVars() + }) + + ginkgo.AfterEach(func() { + mockCtrl.Finish() + }) + + ginkgo.Context("for an existing VPC", func() { + var dummyVPC infrav1.VPC + + ginkgo.BeforeEach(func() { + dummyVPC = infrav1.VPC{ + ID: "vpc-123", + Name: "test-vpc", + CIDR: "10.0.0.0/16", + } + }) + + ginkgo.It("resolves VPC by ID", func() { + dummyCSVPC := &csapi.VPC{ + Id: dummyVPC.ID, + Name: dummyVPC.Name, + Cidr: dummyVPC.CIDR, + } + + vs.EXPECT().GetVPCByID(dummyVPC.ID, gomock.Any()).Return(dummyCSVPC, 1, nil) + + gomega.Ω(client.ResolveVPC(&dummyVPC)).Should(gomega.Succeed()) + gomega.Ω(dummyVPC.Name).Should(gomega.Equal("test-vpc")) + }) + + ginkgo.It("resolves VPC by Name", func() { + dummyVPC.ID = "" // Clear ID to test by name + + dummyCSVPC := &csapi.VPC{ + Id: "vpc-123", + Name: dummyVPC.Name, + Cidr: dummyVPC.CIDR, + } + + vs.EXPECT().GetVPCByName(dummyVPC.Name, gomock.Any()).Return(dummyCSVPC, 1, nil) + + gomega.Ω(client.ResolveVPC(&dummyVPC)).Should(gomega.Succeed()) + gomega.Ω(dummyVPC.ID).Should(gomega.Equal("vpc-123")) + }) + + ginkgo.It("returns error when VPC not found by ID", func() { + vs.EXPECT().GetVPCByID(dummyVPC.ID, gomock.Any()).Return(nil, 0, nil) + + err := client.ResolveVPC(&dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring(fmt.Sprintf("no VPC found with ID %s", dummyVPC.ID))) + }) + + ginkgo.It("returns error when VPC not found by Name", func() { + dummyVPC.ID = "" // Clear ID to test by name + + vs.EXPECT().GetVPCByName(dummyVPC.Name, gomock.Any()).Return(nil, 0, nil) + + err := client.ResolveVPC(&dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring(fmt.Sprintf("no VPC found with name %s", dummyVPC.Name))) + }) + + ginkgo.It("returns error when GetVPCByID fails", func() { + expectedErr := errors.New("API error") + vs.EXPECT().GetVPCByID(dummyVPC.ID, gomock.Any()).Return(nil, 0, expectedErr) + + err := client.ResolveVPC(&dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring(fmt.Sprintf("failed to get VPC with ID %s", dummyVPC.ID))) + }) + + ginkgo.It("returns error when GetVPCByName fails", func() { + dummyVPC.ID = "" // Clear ID to test by name + expectedErr := errors.New("API error") + vs.EXPECT().GetVPCByName(dummyVPC.Name, gomock.Any()).Return(nil, 0, expectedErr) + + err := client.ResolveVPC(&dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring(fmt.Sprintf("failed to get VPC with name %s", dummyVPC.Name))) + }) + + ginkgo.It("handles nil VPC", func() { + gomega.Ω(client.ResolveVPC(nil)).Should(gomega.Succeed()) + }) + + ginkgo.It("handles empty VPC", func() { + emptyVPC := &infrav1.VPC{} + gomega.Ω(client.ResolveVPC(emptyVPC)).Should(gomega.Succeed()) + }) + }) + + ginkgo.Context("for creating a VPC", func() { + var ( + dummyFD infrav1.CloudStackFailureDomain + dummyVPC infrav1.VPC + ) + + ginkgo.BeforeEach(func() { + dummyFD = infrav1.CloudStackFailureDomain{ + Spec: infrav1.CloudStackFailureDomainSpec{ + Zone: infrav1.CloudStackZoneSpec{ + ID: "zone-123", + }, + }, + } + dummyVPC = infrav1.VPC{ + Name: "test-vpc", + CIDR: "10.0.0.0/16", + } + }) + + ginkgo.It("creates a new VPC successfully", func() { + offeringID := "offering-123" + createVPCParams := &csapi.CreateVPCParams{} + createVPCResponse := &csapi.CreateVPCResponse{ + Id: "vpc-123", + } + + vs.EXPECT().GetVPCOfferingID(cloud.VPCOffering).Return(offeringID, 1, nil) + vs.EXPECT().NewCreateVPCParams(dummyVPC.CIDR, dummyVPC.Name, dummyVPC.Name, offeringID, dummyFD.Spec.Zone.ID).Return(createVPCParams) + vs.EXPECT().CreateVPC(createVPCParams).Return(createVPCResponse, nil) + rs.EXPECT().NewCreateTagsParams(gomock.Any(), gomock.Any(), gomock.Any()).Return(&csapi.CreateTagsParams{}) + rs.EXPECT().CreateTags(gomock.Any()).Return(&csapi.CreateTagsResponse{}, nil) + + gomega.Ω(client.CreateVPC(&dummyFD, &dummyVPC)).Should(gomega.Succeed()) + gomega.Ω(dummyVPC.ID).Should(gomega.Equal("vpc-123")) + }) + + ginkgo.It("returns error when VPC offering cannot be fetched", func() { + expectedErr := errors.New("failed to get VPC offering") + vs.EXPECT().GetVPCOfferingID(cloud.VPCOffering).Return("", 0, expectedErr) + + err := client.CreateVPC(&dummyFD, &dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.Equal(expectedErr.Error())) + }) + + ginkgo.It("returns error when multiple VPC offerings found", func() { + vs.EXPECT().GetVPCOfferingID(cloud.VPCOffering).Return("", 2, nil) + + err := client.CreateVPC(&dummyFD, &dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.Equal("found more than one vpc offering")) + }) + + ginkgo.It("returns error when CreateVPC fails", func() { + offeringID := "offering-123" + createVPCParams := &csapi.CreateVPCParams{} + expectedErr := errors.New("API error") + + vs.EXPECT().GetVPCOfferingID(cloud.VPCOffering).Return(offeringID, 1, nil) + vs.EXPECT().NewCreateVPCParams(dummyVPC.CIDR, dummyVPC.Name, dummyVPC.Name, offeringID, dummyFD.Spec.Zone.ID).Return(createVPCParams) + vs.EXPECT().CreateVPC(createVPCParams).Return(nil, expectedErr) + + err := client.CreateVPC(&dummyFD, &dummyVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.ContainSubstring(fmt.Sprintf("creating VPC with name %s", dummyVPC.Name))) + }) + + ginkgo.It("returns error when VPC is nil", func() { + err := client.CreateVPC(&dummyFD, nil) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.Equal("VPC name must be specified")) + }) + + ginkgo.It("returns error when VPC name is empty", func() { + emptyNameVPC := &infrav1.VPC{ + CIDR: "10.0.0.0/16", + } + err := client.CreateVPC(&dummyFD, emptyNameVPC) + gomega.Ω(err).ShouldNot(gomega.Succeed()) + gomega.Ω(err.Error()).Should(gomega.Equal("VPC name must be specified")) + }) + }) +}) diff --git a/test/e2e/common.go b/test/e2e/common.go index d1aafea6..0483359a 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -65,8 +65,8 @@ const ( ) const ( - ControlPlaneIndicator = "control-plane" - MachineDeploymentIndicator = "md" + ControlPlaneIndicator = "-control-plane-" + MachineDeploymentIndicator = "-md-" DataVolumePrefix = "DATA-" ) @@ -263,7 +263,7 @@ func GetACSVersion(client *cloudstack.CloudStackClient) (string, error) { } func DestroyOneMachine(client *cloudstack.CloudStackClient, clusterName string, machineType string) { - matcher := clusterName + "-" + machineType + matcher := clusterName + machineType Byf("Listing machines with %q", matcher) listResp, err := client.VirtualMachine.ListVirtualMachines(client.VirtualMachine.NewListVirtualMachinesParams()) @@ -396,6 +396,34 @@ func CheckNetworkExists(client *cloudstack.CloudStackClient, networkName string) return count == 1, nil } +func CheckVPCExists(client *cloudstack.CloudStackClient, vpcName string) (bool, error) { + return CheckVPCExistsInProject(client, vpcName, "") +} + +func CheckVPCExistsInProject(client *cloudstack.CloudStackClient, vpcName string, project string) (bool, error) { + p := client.VPC.NewListVPCsParams() + p.SetName(vpcName) + + if project != "" { + projectID, _, err := client.Project.GetProjectID(project) + if err != nil { + Fail("Failed to get project: " + err.Error()) + } + p.SetProjectid(projectID) + } + + listResp, err := client.VPC.ListVPCs(p) + if err != nil { + if strings.Contains(err.Error(), "No match found for") { + return false, nil + } + return false, err + } else if listResp.Count > 1 { + return false, fmt.Errorf("Expected 0-1 VPC with name %s, but got %d.", vpcName, listResp.Count) + } + return listResp.Count == 1, nil +} + func CreateCloudStackClient(ctx context.Context, kubeConfigPath string) *cloudstack.CloudStackClient { By("Getting a CloudStack client secret") secret := &corev1.Secret{} diff --git a/test/e2e/config/cloudstack.yaml b/test/e2e/config/cloudstack.yaml index 8c428453..7548fdd2 100644 --- a/test/e2e/config/cloudstack.yaml +++ b/test/e2e/config/cloudstack.yaml @@ -88,6 +88,7 @@ providers: - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-resource-cleanup.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-second-cluster.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-shared-network-kubevip.yaml" + - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering-size-for-non-customized.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-disk-offering-size-for-customized.yaml" @@ -129,6 +130,11 @@ variables: CLOUDSTACK_DOMAIN_NAME: ROOT CLOUDSTACK_INVALID_DOMAIN_NAME: domainXXXX CLOUDSTACK_NETWORK_NAME: isolated-for-e2e-1 + CLOUDSTACK_VPC_NETWORK_NAME: vpc-isolated-for-e2e-1 + CLOUDSTACK_VPC_NAME: vpc-for-e2e-1 + CLOUDSTACK_VPC_CIDR: 10.10.0.0/16 + CLOUDSTACK_GATEWAY: 10.10.0.1 + CLOUDSTACK_NETMASK: 255.255.255.0 CLOUDSTACK_NEW_NETWORK_NAME: isolated-for-e2e-new CLOUDSTACK_SHARED_NETWORK_NAME: Shared1 CLUSTER_ENDPOINT_IP: 172.16.2.199 @@ -167,8 +173,8 @@ intervals: conformance/wait-worker-nodes: ["20m", "10s"] default/wait-errors: ["5m", "10s"] - default/wait-controllers: ["5m", "10s"] - default/wait-cluster: ["5m", "10s"] + default/wait-controllers: ["10m", "10s"] + default/wait-cluster: ["10m", "10s"] default/wait-control-plane: ["20m", "10s"] default/wait-worker-nodes: ["20m", "10s"] default/wait-delete-cluster: ["20m", "10s"] diff --git a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/cloudstack-cluster.yaml b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/cloudstack-cluster.yaml index 510f3f97..a64b61fa 100644 --- a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/cloudstack-cluster.yaml +++ b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/cloudstack-cluster.yaml @@ -11,7 +11,12 @@ spec: zone: name : ${CLOUDSTACK_ZONE_NAME} network: - name: ${CLOUDSTACK_NETWORK_NAME} + name: ${CLOUDSTACK_PROJECT_NAME}-${CLOUDSTACK_VPC_NETWORK_NAME} + gateway: ${CLOUDSTACK_GATEWAY} + netmask: ${CLOUDSTACK_NETMASK} + vpc: + name: ${CLOUDSTACK_PROJECT_NAME}-${CLOUDSTACK_VPC_NAME} + cidr: ${CLOUDSTACK_VPC_CIDR} project: ${CLOUDSTACK_PROJECT_NAME} controlPlaneEndpoint: host: "" diff --git a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml index a1ee84b4..4d4a1375 100644 --- a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml +++ b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml @@ -11,7 +11,7 @@ spec: template: name: ${CLOUDSTACK_TEMPLATE_NAME} sshKey: ${CLOUDSTACK_SSH_KEY_NAME} - affinity: pro + affinity: anti --- apiVersion: infrastructure.cluster.x-k8s.io/v1beta3 kind: CloudStackMachineTemplate @@ -25,4 +25,4 @@ spec: template: name: ${CLOUDSTACK_TEMPLATE_NAME} sshKey: ${CLOUDSTACK_SSH_KEY_NAME} - affinity: pro + affinity: anti diff --git a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/cluster-with-vpc-network.yaml b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/cluster-with-vpc-network.yaml new file mode 100644 index 00000000..fae538b1 --- /dev/null +++ b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/cluster-with-vpc-network.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta3 +kind: CloudStackCluster +metadata: + name: ${CLUSTER_NAME} +spec: + failureDomains: + - name: ${CLOUDSTACK_FD1_NAME} + acsEndpoint: + name: ${CLOUDSTACK_FD1_SECRET_NAME} + namespace: default + zone: + name: ${CLOUDSTACK_ZONE_NAME} + network: + name: ${CLOUDSTACK_NETWORK_NAME} + gateway: ${CLOUDSTACK_GATEWAY} + netmask: ${CLOUDSTACK_NETMASK} + vpc: + name: ${CLOUDSTACK_VPC_NAME} + cidr: ${CLOUDSTACK_VPC_CIDR} + controlPlaneEndpoint: + host: "" + port: 6443 diff --git a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/kustomization.yaml b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/kustomization.yaml new file mode 100644 index 00000000..288f5fb1 --- /dev/null +++ b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-vpc-network/kustomization.yaml @@ -0,0 +1,6 @@ +bases: + - ../bases/cluster-with-kcp.yaml + - ../bases/md.yaml + +patchesStrategicMerge: +- ./cluster-with-vpc-network.yaml diff --git a/test/e2e/project.go b/test/e2e/project.go index bcbf7ce3..8ddf5f2f 100644 --- a/test/e2e/project.go +++ b/test/e2e/project.go @@ -41,6 +41,7 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { cancelWatches context.CancelFunc clusterResources *clusterctl.ApplyClusterTemplateAndWaitResult affinityIds []string + vpcName string ) BeforeEach(func() { @@ -61,11 +62,15 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { input = inputGetter() projectName = os.Getenv("CLOUDSTACK_PROJECT_NAME") + vpcName = fmt.Sprintf("%s-%s", os.Getenv("CLOUDSTACK_PROJECT_NAME"), input.E2EConfig.GetVariable("CLOUDSTACK_VPC_NAME")) csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) project, _, err := csClient.Project.GetProjectByName(projectName) if (err != nil) || (project == nil) { Skip("Failed to fetch project") } + + // Initialize affinityIds to an empty slice to avoid nil checks + affinityIds = make([]string, 0) }) It("Should create a cluster in a project", func() { @@ -81,16 +86,31 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { Namespace: namespace.Name, ClusterName: fmt.Sprintf("%s-%s", specName, util.RandomString(6)), KubernetesVersion: input.E2EConfig.GetVariable(KubernetesVersion), - ControlPlaneMachineCount: pointer.Int64Ptr(1), - WorkerMachineCount: pointer.Int64Ptr(1), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(2), }, WaitForClusterIntervals: input.E2EConfig.GetIntervals(specName, "wait-cluster"), WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"), WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"), }, clusterResources) + // Ensure the cluster was created successfully before proceeding with checks + Expect(clusterResources.Cluster).ToNot(BeNil(), "Cluster was not created successfully") + + By("Checking affinity groups and VPC in project") csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) - affinityIds = CheckAffinityGroupInProject(csClient, clusterResources.Cluster.Name, "pro", projectName) + + // Check affinity groups + By(fmt.Sprintf("Checking affinity groups for cluster %s in project %s", clusterResources.Cluster.Name, projectName)) + tempAffinityIds := CheckAffinityGroupInProject(csClient, clusterResources.Cluster.Name, "anti", projectName) + Expect(tempAffinityIds).ToNot(BeEmpty(), "No affinity groups found for cluster") + affinityIds = tempAffinityIds + + // Check VPC + By(fmt.Sprintf("Checking if VPC %s exists in project %s", vpcName, projectName)) + exists, err := CheckVPCExistsInProject(csClient, vpcName, projectName) + Expect(err).To(BeNil(), "Error checking VPC existence") + Expect(exists).To(BeTrue(), fmt.Sprintf("VPC %s does not exist in project %s", vpcName, projectName)) }) AfterEach(func() { @@ -98,10 +118,17 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup) csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) - err := CheckAffinityGroupsDeletedInProject(csClient, affinityIds, projectName) - if err != nil { - Fail(err.Error()) + + // Only check for affinity group deletion if affinity groups were created + if len(affinityIds) > 0 { + err := CheckAffinityGroupsDeletedInProject(csClient, affinityIds, projectName) + if err != nil { + Fail(err.Error()) + } + } else { + By("Skipping affinity group deletion check as no affinity groups were created") } + By("PASSED!") }) } diff --git a/test/e2e/vpc_network.go b/test/e2e/vpc_network.go new file mode 100644 index 00000000..44fa1363 --- /dev/null +++ b/test/e2e/vpc_network.go @@ -0,0 +1,96 @@ +/* +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 e2e + +import ( + "context" + "fmt" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/pointer" + + "sigs.k8s.io/cluster-api/test/framework/clusterctl" + "sigs.k8s.io/cluster-api/util" +) + +// VPCNetworkSpec implements a test that verifies cluster creation in a VPC network. +func VPCNetworkSpec(ctx context.Context, inputGetter func() CommonSpecInput) { + var ( + specName = "vpc-network" + input CommonSpecInput + namespace *corev1.Namespace + cancelWatches context.CancelFunc + clusterResources *clusterctl.ApplyClusterTemplateAndWaitResult + vpcName = "vpc-for-e2e-1" + ) + + BeforeEach(func() { + Expect(ctx).NotTo(BeNil(), "ctx is required for %s spec", specName) + input = inputGetter() + Expect(input.E2EConfig).ToNot(BeNil(), "Invalid argument. input.E2EConfig can't be nil when calling %s spec", specName) + Expect(input.ClusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. input.ClusterctlConfigPath must be an existing file when calling %s spec", specName) + Expect(input.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName) + Expect(os.MkdirAll(input.ArtifactFolder, 0750)).To(Succeed(), "Invalid argument. input.ArtifactFolder can't be created for %s spec", specName) + Expect(input.E2EConfig.Variables).To(HaveKey(KubernetesVersion)) + Expect(input.E2EConfig.Variables).To(HaveValidVersion(input.E2EConfig.GetVariable(KubernetesVersion))) + + // Setup a Namespace where to host objects for this spec and create a watcher for the namespace events. + namespace, cancelWatches = setupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder) + clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult) + }) + + It("Should successfully create a cluster in a VPC network", func() { + By("Creating a workload cluster in a VPC network") + + clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ + ClusterProxy: input.BootstrapClusterProxy, + CNIManifestPath: input.E2EConfig.GetVariable(CNIPath), + ConfigCluster: clusterctl.ConfigClusterInput{ + LogFolder: filepath.Join(input.ArtifactFolder, "clusters", input.BootstrapClusterProxy.GetName()), + ClusterctlConfigPath: input.ClusterctlConfigPath, + KubeconfigPath: input.BootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Flavor: specName, + Namespace: namespace.Name, + ClusterName: fmt.Sprintf("%s-%s", specName, util.RandomString(6)), + KubernetesVersion: input.E2EConfig.GetVariable(KubernetesVersion), + ControlPlaneMachineCount: pointer.Int64(1), + WorkerMachineCount: pointer.Int64(1), + }, + WaitForClusterIntervals: input.E2EConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: input.E2EConfig.GetIntervals(specName, "wait-control-plane"), + WaitForMachineDeployments: input.E2EConfig.GetIntervals(specName, "wait-worker-nodes"), + }, clusterResources) + + csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) + + exists, err := CheckVPCExists(csClient, vpcName) + Expect(err).To(BeNil()) + Expect(exists).To(BeTrue()) + + By("PASSED!") + }) + + AfterEach(func() { + // Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself. + dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup) + }) +} diff --git a/test/e2e/vpc_network_test.go b/test/e2e/vpc_network_test.go new file mode 100644 index 00000000..92cab356 --- /dev/null +++ b/test/e2e/vpc_network_test.go @@ -0,0 +1,40 @@ +//go:build e2e +// +build e2e + +/* +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 e2e + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("When testing clusters in a VPC network", func() { + + VPCNetworkSpec(context.TODO(), func() CommonSpecInput { + return CommonSpecInput{ + E2EConfig: e2eConfig, + ClusterctlConfigPath: clusterctlConfigPath, + BootstrapClusterProxy: bootstrapClusterProxy, + ArtifactFolder: artifactFolder, + SkipCleanup: skipCleanup, + } + }) + +}) From 3a28780e6d6419ad2123ee3d5ef67a6e72e40e19 Mon Sep 17 00:00:00 2001 From: Vishesh Date: Wed, 19 Mar 2025 15:22:28 +0530 Subject: [PATCH 4/4] Remove affinity groups from project tests --- .../v1beta3/cluster-template-project/md.yaml | 2 -- test/e2e/project.go | 23 +------------------ 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml index 4d4a1375..87a44f35 100644 --- a/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml +++ b/test/e2e/data/infrastructure-cloudstack/v1beta3/cluster-template-project/md.yaml @@ -11,7 +11,6 @@ spec: template: name: ${CLOUDSTACK_TEMPLATE_NAME} sshKey: ${CLOUDSTACK_SSH_KEY_NAME} - affinity: anti --- apiVersion: infrastructure.cluster.x-k8s.io/v1beta3 kind: CloudStackMachineTemplate @@ -25,4 +24,3 @@ spec: template: name: ${CLOUDSTACK_TEMPLATE_NAME} sshKey: ${CLOUDSTACK_SSH_KEY_NAME} - affinity: anti diff --git a/test/e2e/project.go b/test/e2e/project.go index 8ddf5f2f..4398e96b 100644 --- a/test/e2e/project.go +++ b/test/e2e/project.go @@ -40,7 +40,6 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { namespace *corev1.Namespace cancelWatches context.CancelFunc clusterResources *clusterctl.ApplyClusterTemplateAndWaitResult - affinityIds []string vpcName string ) @@ -69,8 +68,6 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { Skip("Failed to fetch project") } - // Initialize affinityIds to an empty slice to avoid nil checks - affinityIds = make([]string, 0) }) It("Should create a cluster in a project", func() { @@ -97,15 +94,9 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { // Ensure the cluster was created successfully before proceeding with checks Expect(clusterResources.Cluster).ToNot(BeNil(), "Cluster was not created successfully") - By("Checking affinity groups and VPC in project") + By("Checking VPC in project") csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) - // Check affinity groups - By(fmt.Sprintf("Checking affinity groups for cluster %s in project %s", clusterResources.Cluster.Name, projectName)) - tempAffinityIds := CheckAffinityGroupInProject(csClient, clusterResources.Cluster.Name, "anti", projectName) - Expect(tempAffinityIds).ToNot(BeEmpty(), "No affinity groups found for cluster") - affinityIds = tempAffinityIds - // Check VPC By(fmt.Sprintf("Checking if VPC %s exists in project %s", vpcName, projectName)) exists, err := CheckVPCExistsInProject(csClient, vpcName, projectName) @@ -117,18 +108,6 @@ func ProjectSpec(ctx context.Context, inputGetter func() CommonSpecInput) { // Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself. dumpSpecResourcesAndCleanup(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, namespace, cancelWatches, clusterResources.Cluster, input.E2EConfig.GetIntervals, input.SkipCleanup) - csClient := CreateCloudStackClient(ctx, input.BootstrapClusterProxy.GetKubeconfigPath()) - - // Only check for affinity group deletion if affinity groups were created - if len(affinityIds) > 0 { - err := CheckAffinityGroupsDeletedInProject(csClient, affinityIds, projectName) - if err != nil { - Fail(err.Error()) - } - } else { - By("Skipping affinity group deletion check as no affinity groups were created") - } - By("PASSED!") }) }