diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index cf94a97c..a053a29a 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -506,6 +506,7 @@ 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.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain requires manual conversion: does not exist in peer-type return nil } @@ -518,7 +519,6 @@ func autoConvert_v1beta1_CloudStackIsolatedNetworkStatus_To_v1beta3_CloudStackIs } func autoConvert_v1beta3_CloudStackIsolatedNetworkStatus_To_v1beta1_CloudStackIsolatedNetworkStatus(in *v1beta3.CloudStackIsolatedNetworkStatus, out *CloudStackIsolatedNetworkStatus, s conversion.Scope) error { - // WARNING: in.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.PublicIPAddress requires manual conversion: does not exist in peer-type out.PublicIPID = in.PublicIPID out.LBRuleID = in.LBRuleID @@ -973,6 +973,7 @@ 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.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain 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 8e1aba4b..de8d49d4 100644 --- a/api/v1beta2/zz_generated.conversion.go +++ b/api/v1beta2/zz_generated.conversion.go @@ -818,6 +818,7 @@ func autoConvert_v1beta3_CloudStackIsolatedNetworkSpec_To_v1beta2_CloudStackIsol out.ID = in.ID out.ControlPlaneEndpoint = in.ControlPlaneEndpoint out.FailureDomainName = in.FailureDomainName + // WARNING: in.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain requires manual conversion: does not exist in peer-type return nil } @@ -830,7 +831,6 @@ func autoConvert_v1beta2_CloudStackIsolatedNetworkStatus_To_v1beta3_CloudStackIs } func autoConvert_v1beta3_CloudStackIsolatedNetworkStatus_To_v1beta2_CloudStackIsolatedNetworkStatus(in *v1beta3.CloudStackIsolatedNetworkStatus, out *CloudStackIsolatedNetworkStatus, s conversion.Scope) error { - // WARNING: in.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.PublicIPAddress requires manual conversion: does not exist in peer-type out.PublicIPID = in.PublicIPID out.LBRuleID = in.LBRuleID @@ -1290,6 +1290,7 @@ 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.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta3/cloudstackcluster_webhook.go b/api/v1beta3/cloudstackcluster_webhook.go index 3158394e..0ec6af1b 100644 --- a/api/v1beta3/cloudstackcluster_webhook.go +++ b/api/v1beta3/cloudstackcluster_webhook.go @@ -18,6 +18,7 @@ package v1beta3 import ( "fmt" + "net" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -79,6 +80,12 @@ func (r *CloudStackCluster) ValidateCreate() (admission.Warnings, error) { field.NewPath("spec", "failureDomains", "ACSEndpoint"), "Name and Namespace are required")) } + if fdSpec.Zone.Network.CIDR != "" { + if _, errMsg := ValidateCIDR(fdSpec.Zone.Network.CIDR); errMsg != nil { + errorList = append(errorList, field.Invalid( + field.NewPath("spec", "failureDomains", "Zone", "Network"), fdSpec.Zone.Network.CIDR, fmt.Sprintf("must be valid CIDR: %s", errMsg.Error()))) + } + } if fdSpec.Zone.Network.Domain != "" { for _, errMsg := range validation.IsDNS1123Subdomain(fdSpec.Zone.Network.Domain) { errorList = append(errorList, field.Invalid( @@ -127,6 +134,13 @@ func (r *CloudStackCluster) ValidateUpdate(old runtime.Object) (admission.Warnin return nil, webhookutil.AggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, errorList) } +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *CloudStackCluster) ValidateDelete() (admission.Warnings, error) { + cloudstackclusterlog.V(1).Info("entered validate delete webhook", "api resource name", r.Name) + // No deletion validations. Deletion webhook not enabled. + return nil, nil +} + // ValidateFailureDomainUpdates verifies that at least one failure domain has not been deleted, and // failure domains that are held over have not been modified. func ValidateFailureDomainUpdates(oldFDs, newFDs []CloudStackFailureDomainSpec) *field.Error { @@ -165,9 +179,11 @@ func FailureDomainsEqual(fd1, fd2 CloudStackFailureDomainSpec) bool { fd1.Zone.Network.Domain == fd2.Zone.Network.Domain } -// ValidateDelete implements webhook.Validator so a webhook will be registered for the type -func (r *CloudStackCluster) ValidateDelete() (admission.Warnings, error) { - cloudstackclusterlog.V(1).Info("entered validate delete webhook", "api resource name", r.Name) - // No deletion validations. Deletion webhook not enabled. - return nil, nil +// ValidateCIDR validates whether a CIDR matches the conventions expected by net.ParseCIDR +func ValidateCIDR(cidr string) (*net.IPNet, error) { + _, net, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + return net, nil } diff --git a/api/v1beta3/cloudstackfailuredomain_types.go b/api/v1beta3/cloudstackfailuredomain_types.go index be3f5e48..893f0f88 100644 --- a/api/v1beta3/cloudstackfailuredomain_types.go +++ b/api/v1beta3/cloudstackfailuredomain_types.go @@ -54,6 +54,10 @@ type Network struct { // Cloudstack Network Name the cluster is built in. Name string `json:"name"` + // CIDR is the IP address range of the network. + //+optional + CIDR string `json:"cidr,omitempty"` + // Domain is the DNS domain name used for all instances in the network. //+optional Domain string `json:"domain,omitempty"` diff --git a/api/v1beta3/cloudstackisolatednetwork_types.go b/api/v1beta3/cloudstackisolatednetwork_types.go index 03ff72f7..7c2f45ce 100644 --- a/api/v1beta3/cloudstackisolatednetwork_types.go +++ b/api/v1beta3/cloudstackisolatednetwork_types.go @@ -40,6 +40,10 @@ type CloudStackIsolatedNetworkSpec struct { // FailureDomainName -- the FailureDomain the network is placed in. FailureDomainName string `json:"failureDomainName"` + // CIDR is the IP range of the isolated network. + //+optional + CIDR string `json:"cidr,omitempty"` + // Domain is the DNS domain name used for all instances in the isolated network. //+optional Domain string `json:"domain,omitempty"` @@ -47,9 +51,6 @@ type CloudStackIsolatedNetworkSpec struct { // CloudStackIsolatedNetworkStatus defines the observed state of CloudStackIsolatedNetwork type CloudStackIsolatedNetworkStatus struct { - // The CIDR of the assigned subnet. - CIDR string `json:"cidr,omitempty"` - // The outgoing IP of the isolated network. PublicIPAddress string `json:"publicIPAddress,omitempty"` 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 1351c750..ed1de549 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml @@ -439,6 +439,9 @@ spec: network: description: The network within the Zone to use. properties: + cidr: + description: CIDR is the IP address range of the network. + type: string domain: description: Domain is the DNS domain name used for all instances in the network. 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 5d59fa92..e794b516 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml @@ -175,6 +175,9 @@ spec: network: description: The network within the Zone to use. properties: + cidr: + description: CIDR is the IP address range of the network. + type: string domain: description: Domain is the DNS domain name used for all instances in the network. 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 443c5994..b7032f5b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackisolatednetworks.yaml @@ -188,6 +188,9 @@ spec: description: CloudStackIsolatedNetworkSpec defines the desired state of CloudStackIsolatedNetwork properties: + cidr: + description: CIDR is the IP range of the isolated network. + type: string controlPlaneEndpoint: description: The kubernetes control plane endpoint. properties: @@ -240,9 +243,6 @@ spec: - ipAddress - ipAddressID type: object - cidr: - description: The CIDR of the assigned subnet. - type: string loadBalancerRuleID: description: |- Deprecated: The ID of the lb rule used to assign VMs to the lb. diff --git a/controllers/cloudstackfailuredomain_controller.go b/controllers/cloudstackfailuredomain_controller.go index fb5f7384..519abfc4 100644 --- a/controllers/cloudstackfailuredomain_controller.go +++ b/controllers/cloudstackfailuredomain_controller.go @@ -106,11 +106,10 @@ func (r *CloudStackFailureDomainReconciliationRunner) Reconcile() (retRes ctrl.R // CloudStackIsolatedNetwork to manage the many intricacies and wait until CloudStackIsolatedNetwork is ready. if r.ReconciliationSubject.Spec.Zone.Network.ID == "" || r.ReconciliationSubject.Spec.Zone.Network.Type == infrav1.NetworkTypeIsolated { - netName := r.ReconciliationSubject.Spec.Zone.Network.Name if res, err := r.GenerateIsolatedNetwork( - netName, r.ReconciliationSubject.Spec.Name, r.ReconciliationSubject.Spec.Zone.Network.Domain)(); r.ShouldReturn(res, err) { + r.ReconciliationSubject.Spec.Zone.Network, r.ReconciliationSubject.Spec.Name)(); r.ShouldReturn(res, err) { return res, err - } else if res, err := r.GetObjectByName(r.IsoNetMetaName(netName), r.IsoNet)(); r.ShouldReturn(res, err) { + } else if res, err := r.GetObjectByName(r.IsoNetMetaName(r.ReconciliationSubject.Spec.Zone.Network.Name), r.IsoNet)(); r.ShouldReturn(res, err) { return res, err } if r.IsoNet.Name == "" { diff --git a/controllers/utils/isolated_network.go b/controllers/utils/isolated_network.go index ef91e0ed..3c6bc018 100644 --- a/controllers/utils/isolated_network.go +++ b/controllers/utils/isolated_network.go @@ -32,17 +32,20 @@ func (r *ReconciliationRunner) IsoNetMetaName(name string) string { return strings.TrimSuffix(str, "-") } -// GenerateIsolatedNetwork of the passed name that's owned by the ReconciliationSubject. -func (r *ReconciliationRunner) GenerateIsolatedNetwork(name, fdName, domainName string) CloudStackReconcilerMethod { +// GenerateIsolatedNetwork creates a CloudStackIsolatedNetwork object that is owned by the ReconciliationSubject. +func (r *ReconciliationRunner) GenerateIsolatedNetwork(network infrav1.Network, fdName string) CloudStackReconcilerMethod { return func() (ctrl.Result, error) { - lowerName := strings.ToLower(name) + lowerName := strings.ToLower(network.Name) metaName := fmt.Sprintf("%s-%s", r.CSCluster.Name, lowerName) csIsoNet := &infrav1.CloudStackIsolatedNetwork{} csIsoNet.ObjectMeta = r.NewChildObjectMeta(metaName) csIsoNet.Spec.Name = lowerName csIsoNet.Spec.FailureDomainName = fdName - if domainName != "" { - csIsoNet.Spec.Domain = strings.ToLower(domainName) + if network.CIDR != "" { + csIsoNet.Spec.CIDR = network.CIDR + } + if network.Domain != "" { + csIsoNet.Spec.Domain = strings.ToLower(network.Domain) } csIsoNet.Spec.ControlPlaneEndpoint.Host = r.CSCluster.Spec.ControlPlaneEndpoint.Host csIsoNet.Spec.ControlPlaneEndpoint.Port = r.CSCluster.Spec.ControlPlaneEndpoint.Port diff --git a/pkg/cloud/isolated_network.go b/pkg/cloud/isolated_network.go index 30aeb526..738c1cbe 100644 --- a/pkg/cloud/isolated_network.go +++ b/pkg/cloud/isolated_network.go @@ -18,6 +18,7 @@ package cloud import ( "fmt" + "net" "slices" "strconv" "strings" @@ -123,13 +124,24 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN if isoNet.Spec.Domain != "" { p.SetNetworkdomain(isoNet.Spec.Domain) } + if isoNet.Spec.CIDR != "" { + m, err := parseCIDR(isoNet.Spec.CIDR) + if err != nil { + return errors.Wrap(err, "parsing CIDR") + } + // Set the needed IP subnet config + p.SetGateway(m["gateway"]) + p.SetNetmask(m["netmask"]) + p.SetStartip(m["startip"]) + p.SetEndip(m["endip"]) + } resp, err := c.cs.Network.CreateNetwork(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) return errors.Wrapf(err, "creating network with name %s", isoNet.Spec.Name) } isoNet.Spec.ID = resp.Id - isoNet.Status.CIDR = resp.Cidr + isoNet.Spec.CIDR = resp.Cidr return c.AddCreatedByCAPCTag(ResourceTypeNetwork, isoNet.Spec.ID) } @@ -201,7 +213,7 @@ func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) ( "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 - isoNet.Status.CIDR = netDetails.Cidr + isoNet.Spec.CIDR = netDetails.Cidr return nil } @@ -213,7 +225,7 @@ func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) ( return multierror.Append(retErr, errors.Errorf("expected 1 Network with UUID %s, but got %d", isoNet.Spec.ID, count)) } isoNet.Spec.Name = netDetails.Name - isoNet.Status.CIDR = netDetails.Cidr + isoNet.Spec.CIDR = netDetails.Cidr return nil } @@ -688,13 +700,15 @@ func (c *client) GetOrCreateIsolatedNetwork( csCluster *infrav1.CloudStackCluster, ) error { // Get or create the isolated network itself and resolve details into passed custom resources. - net := isoNet.Network() - if err := c.ResolveNetwork(net); err != nil { // Doesn't exist, create isolated network. + network := isoNet.Network() + if err := c.ResolveNetwork(network); err != nil { // Doesn't exist, create isolated network. if err = c.CreateIsolatedNetwork(fd, isoNet); err != nil { return errors.Wrap(err, "creating a new isolated network") } - } else { // Network existed and was resolved. Set ID on isoNet CloudStackIsolatedNetwork in case it only had name set. - isoNet.Spec.ID = net.ID + } else { + // Network existed and was resolved. Set ID on isoNet CloudStackIsolatedNetwork in case it only had name set. + isoNet.Spec.ID = network.ID + isoNet.Spec.CIDR = network.CIDR } // Tag the created network. @@ -910,3 +924,24 @@ func (c *client) DisassociatePublicIPAddress(ipAddressID string) error { return err } + +// parseCIDR parses a CIDR-formatted string into the components required for CreateNetwork. +func parseCIDR(cidr string) (map[string]string, error) { + m := make(map[string]string, 4) + + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("unable to parse cidr %s: %s", cidr, err) + } + + msk := ipnet.Mask + sub := ip.Mask(msk) + + m["netmask"] = fmt.Sprintf("%d.%d.%d.%d", msk[0], msk[1], msk[2], msk[3]) + m["gateway"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+1) + m["startip"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+2) + m["endip"] = fmt.Sprintf("%d.%d.%d.%d", + sub[0]+(0xff-msk[0]), sub[1]+(0xff-msk[1]), sub[2]+(0xff-msk[2]), sub[3]+(0xff-msk[3]-1)) + + return m, nil +} diff --git a/pkg/cloud/isolated_network_test.go b/pkg/cloud/isolated_network_test.go index 6e45afef..7b4d2bc4 100644 --- a/pkg/cloud/isolated_network_test.go +++ b/pkg/cloud/isolated_network_test.go @@ -851,7 +851,6 @@ var _ = Describe("Network", func() { dummies.CSISONet1.Status.PublicIPID = dummies.PublicIPID dummies.CSISONet1.Status.PublicIPAddress = "10.11.12.13/32" dummies.CSISONet1.Status.APIServerLoadBalancer.IPAddressID = dummies.LoadBalancerIPID - dummies.CSISONet1.Status.CIDR = "10.1.0.0/24" fs.EXPECT().NewListFirewallRulesParams().Return(&csapi.ListFirewallRulesParams{}) fs.EXPECT().ListFirewallRules(gomock.Any()).Return( &csapi.ListFirewallRulesResponse{FirewallRules: []*csapi.FirewallRule{}}, nil) diff --git a/pkg/cloud/network.go b/pkg/cloud/network.go index 7c8d6256..9e62f858 100644 --- a/pkg/cloud/network.go +++ b/pkg/cloud/network.go @@ -52,6 +52,7 @@ 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.CIDR = netDetails.Cidr net.Domain = netDetails.Networkdomain return nil } @@ -67,6 +68,7 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { net.Name = netDetails.Name net.ID = netDetails.Id net.Type = netDetails.Type + net.CIDR = netDetails.Cidr net.Domain = netDetails.Networkdomain return nil }