diff --git a/api/v1alpha5/conversion.go b/api/v1alpha5/conversion.go index b4855587e0..ddaf5d71ab 100644 --- a/api/v1alpha5/conversion.go +++ b/api/v1alpha5/conversion.go @@ -196,6 +196,14 @@ func Convert_v1alpha8_OpenStackClusterSpec_To_v1alpha5_OpenStackClusterSpec(in * out.ExternalNetworkID = in.ExternalNetwork.ID } + if in.Subnets != nil { + if len(in.Subnets) >= 1 { + if err := Convert_v1alpha8_SubnetFilter_To_v1alpha5_SubnetFilter(&in.Subnets[0], &out.Subnet, s); err != nil { + return err + } + } + } + return nil } @@ -211,6 +219,15 @@ func Convert_v1alpha5_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec(in * } } + emptySubnet := SubnetFilter{} + if in.Subnet != emptySubnet { + subnet := infrav1.SubnetFilter{} + if err := Convert_v1alpha5_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &subnet, s); err != nil { + return err + } + out.Subnets = []infrav1.SubnetFilter{subnet} + } + return nil } diff --git a/api/v1alpha5/conversion_test.go b/api/v1alpha5/conversion_test.go index f8ad758fca..e4357782c4 100644 --- a/api/v1alpha5/conversion_test.go +++ b/api/v1alpha5/conversion_test.go @@ -49,7 +49,7 @@ func TestConvertFrom(t *testing.T) { Spec: OpenStackClusterSpec{}, ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - "cluster.x-k8s.io/conversion-data": "{\"spec\":{\"allowAllInClusterTraffic\":false,\"apiServerLoadBalancer\":{},\"cloudName\":\"\",\"controlPlaneEndpoint\":{\"host\":\"\",\"port\":0},\"disableAPIServerFloatingIP\":false,\"disableExternalNetwork\":false,\"externalNetwork\":{},\"managedSecurityGroups\":false,\"network\":{},\"subnet\":{}},\"status\":{\"ready\":false}}", + "cluster.x-k8s.io/conversion-data": "{\"spec\":{\"allowAllInClusterTraffic\":false,\"apiServerLoadBalancer\":{},\"cloudName\":\"\",\"controlPlaneEndpoint\":{\"host\":\"\",\"port\":0},\"disableAPIServerFloatingIP\":false,\"disableExternalNetwork\":false,\"externalNetwork\":{},\"managedSecurityGroups\":false,\"network\":{}},\"status\":{\"ready\":false}}", }, }, }, @@ -64,7 +64,7 @@ func TestConvertFrom(t *testing.T) { Spec: OpenStackClusterTemplateSpec{}, ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ - "cluster.x-k8s.io/conversion-data": "{\"spec\":{\"template\":{\"spec\":{\"allowAllInClusterTraffic\":false,\"apiServerLoadBalancer\":{},\"cloudName\":\"\",\"controlPlaneEndpoint\":{\"host\":\"\",\"port\":0},\"disableAPIServerFloatingIP\":false,\"disableExternalNetwork\":false,\"externalNetwork\":{},\"managedSecurityGroups\":false,\"network\":{},\"subnet\":{}}}}}", + "cluster.x-k8s.io/conversion-data": "{\"spec\":{\"template\":{\"spec\":{\"allowAllInClusterTraffic\":false,\"apiServerLoadBalancer\":{},\"cloudName\":\"\",\"controlPlaneEndpoint\":{\"host\":\"\",\"port\":0},\"disableAPIServerFloatingIP\":false,\"disableExternalNetwork\":false,\"externalNetwork\":{},\"managedSecurityGroups\":false,\"network\":{}}}}}", }, }, }, diff --git a/api/v1alpha5/zz_generated.conversion.go b/api/v1alpha5/zz_generated.conversion.go index af5bc6fcbf..b10bd71be9 100644 --- a/api/v1alpha5/zz_generated.conversion.go +++ b/api/v1alpha5/zz_generated.conversion.go @@ -659,9 +659,7 @@ func autoConvert_v1alpha5_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec( if err := Convert_v1alpha5_NetworkFilter_To_v1alpha8_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha5_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnet requires manual conversion: does not exist in peer-type out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) if in.ExternalRouterIPs != nil { in, out := &in.ExternalRouterIPs, &out.ExternalRouterIPs @@ -708,9 +706,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha5_OpenStackClusterSpec( if err := Convert_v1alpha8_NetworkFilter_To_v1alpha5_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha8_SubnetFilter_To_v1alpha5_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnets requires manual conversion: does not exist in peer-type // WARNING: in.NetworkMTU requires manual conversion: does not exist in peer-type out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) if in.ExternalRouterIPs != nil { diff --git a/api/v1alpha6/conversion.go b/api/v1alpha6/conversion.go index 2e2000cfd4..5f05f4c692 100644 --- a/api/v1alpha6/conversion.go +++ b/api/v1alpha6/conversion.go @@ -486,6 +486,14 @@ func Convert_v1alpha8_OpenStackClusterSpec_To_v1alpha6_OpenStackClusterSpec(in * out.ExternalNetworkID = in.ExternalNetwork.ID } + if in.Subnets != nil { + if len(in.Subnets) >= 1 { + if err := Convert_v1alpha8_SubnetFilter_To_v1alpha6_SubnetFilter(&in.Subnets[0], &out.Subnet, s); err != nil { + return err + } + } + } + return nil } @@ -501,6 +509,15 @@ func Convert_v1alpha6_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec(in * } } + emptySubnet := SubnetFilter{} + if in.Subnet != emptySubnet { + subnet := infrav1.SubnetFilter{} + if err := Convert_v1alpha6_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &subnet, s); err != nil { + return err + } + out.Subnets = []infrav1.SubnetFilter{subnet} + } + return nil } diff --git a/api/v1alpha6/zz_generated.conversion.go b/api/v1alpha6/zz_generated.conversion.go index 8a00f25e45..41761e8db2 100644 --- a/api/v1alpha6/zz_generated.conversion.go +++ b/api/v1alpha6/zz_generated.conversion.go @@ -681,9 +681,7 @@ func autoConvert_v1alpha6_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec( if err := Convert_v1alpha6_NetworkFilter_To_v1alpha8_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha6_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnet requires manual conversion: does not exist in peer-type out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) if in.ExternalRouterIPs != nil { in, out := &in.ExternalRouterIPs, &out.ExternalRouterIPs @@ -731,9 +729,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha6_OpenStackClusterSpec( if err := Convert_v1alpha8_NetworkFilter_To_v1alpha6_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha8_SubnetFilter_To_v1alpha6_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnets requires manual conversion: does not exist in peer-type // WARNING: in.NetworkMTU requires manual conversion: does not exist in peer-type out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) if in.ExternalRouterIPs != nil { diff --git a/api/v1alpha7/conversion.go b/api/v1alpha7/conversion.go index 31862c3f86..4538d1cee3 100644 --- a/api/v1alpha7/conversion.go +++ b/api/v1alpha7/conversion.go @@ -383,6 +383,15 @@ func Convert_v1alpha7_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec(in * } } + emptySubnet := SubnetFilter{} + if in.Subnet != emptySubnet { + subnet := infrav1.SubnetFilter{} + if err := Convert_v1alpha7_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &subnet, s); err != nil { + return err + } + out.Subnets = []infrav1.SubnetFilter{subnet} + } + return nil } @@ -396,5 +405,13 @@ func Convert_v1alpha8_OpenStackClusterSpec_To_v1alpha7_OpenStackClusterSpec(in * out.ExternalNetworkID = in.ExternalNetwork.ID } + if in.Subnets != nil { + if len(in.Subnets) >= 1 { + if err := Convert_v1alpha8_SubnetFilter_To_v1alpha7_SubnetFilter(&in.Subnets[0], &out.Subnet, s); err != nil { + return err + } + } + } + return nil } diff --git a/api/v1alpha7/zz_generated.conversion.go b/api/v1alpha7/zz_generated.conversion.go index 2df0972e6e..773d9892fd 100644 --- a/api/v1alpha7/zz_generated.conversion.go +++ b/api/v1alpha7/zz_generated.conversion.go @@ -882,9 +882,7 @@ func autoConvert_v1alpha7_OpenStackClusterSpec_To_v1alpha8_OpenStackClusterSpec( if err := Convert_v1alpha7_NetworkFilter_To_v1alpha8_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha7_SubnetFilter_To_v1alpha8_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnet requires manual conversion: does not exist in peer-type out.NetworkMTU = in.NetworkMTU out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) out.ExternalRouterIPs = *(*[]v1alpha8.ExternalRouterIPParam)(unsafe.Pointer(&in.ExternalRouterIPs)) @@ -923,9 +921,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha7_OpenStackClusterSpec( if err := Convert_v1alpha8_NetworkFilter_To_v1alpha7_NetworkFilter(&in.Network, &out.Network, s); err != nil { return err } - if err := Convert_v1alpha8_SubnetFilter_To_v1alpha7_SubnetFilter(&in.Subnet, &out.Subnet, s); err != nil { - return err - } + // WARNING: in.Subnets requires manual conversion: does not exist in peer-type out.NetworkMTU = in.NetworkMTU out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) out.ExternalRouterIPs = *(*[]ExternalRouterIPParam)(unsafe.Pointer(&in.ExternalRouterIPs)) diff --git a/api/v1alpha8/openstackcluster_types.go b/api/v1alpha8/openstackcluster_types.go index b0b6aaabcf..7430fab1ab 100644 --- a/api/v1alpha8/openstackcluster_types.go +++ b/api/v1alpha8/openstackcluster_types.go @@ -47,8 +47,9 @@ type OpenStackClusterSpec struct { // If NodeCIDR cannot be set this can be used to detect an existing network. Network NetworkFilter `json:"network,omitempty"` - // If NodeCIDR cannot be set this can be used to detect an existing subnet. - Subnet SubnetFilter `json:"subnet,omitempty"` + // If NodeCIDR cannot be set this can be used to detect existing IPv4 and/or IPv6 subnets. + // +kubebuilder:validation:MaxItems=2 + Subnets []SubnetFilter `json:"subnets,omitempty"` // NetworkMTU sets the maximum transmission unit (MTU) value to address fragmentation for the private network ID. // This value will be used only if the Cluster actuator creates the network. diff --git a/api/v1alpha8/zz_generated.deepcopy.go b/api/v1alpha8/zz_generated.deepcopy.go index 6598146562..eebb59993e 100644 --- a/api/v1alpha8/zz_generated.deepcopy.go +++ b/api/v1alpha8/zz_generated.deepcopy.go @@ -372,7 +372,11 @@ func (in *OpenStackClusterSpec) DeepCopyInto(out *OpenStackClusterSpec) { **out = **in } out.Network = in.Network - out.Subnet = in.Subnet + if in.Subnets != nil { + in, out := &in.Subnets, &out.Subnets + *out = make([]SubnetFilter, len(*in)) + copy(*out, *in) + } if in.DNSNameservers != nil { in, out := &in.DNSNameservers, &out.DNSNameservers *out = make([]string, len(*in)) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml index c22d9fcea5..00b1ba6cc9 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml @@ -5507,37 +5507,40 @@ spec: tagsAny: type: string type: object - subnet: + subnets: description: If NodeCIDR cannot be set this can be used to detect - an existing subnet. - properties: - cidr: - type: string - description: - type: string - gateway_ip: - type: string - id: - type: string - ipVersion: - type: integer - ipv6AddressMode: - type: string - ipv6RaMode: - type: string - name: - type: string - notTags: - type: string - notTagsAny: - type: string - projectId: - type: string - tags: - type: string - tagsAny: - type: string - type: object + existing IPv4 and/or IPv6 subnets. + items: + properties: + cidr: + type: string + description: + type: string + gateway_ip: + type: string + id: + type: string + ipVersion: + type: integer + ipv6AddressMode: + type: string + ipv6RaMode: + type: string + name: + type: string + notTags: + type: string + notTagsAny: + type: string + projectId: + type: string + tags: + type: string + tagsAny: + type: string + type: object + maxItems: 2 + type: array tags: description: Tags for all resources in cluster items: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml index 4f96c272aa..cb56c31021 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml @@ -2942,37 +2942,40 @@ spec: tagsAny: type: string type: object - subnet: + subnets: description: If NodeCIDR cannot be set this can be used to - detect an existing subnet. - properties: - cidr: - type: string - description: - type: string - gateway_ip: - type: string - id: - type: string - ipVersion: - type: integer - ipv6AddressMode: - type: string - ipv6RaMode: - type: string - name: - type: string - notTags: - type: string - notTagsAny: - type: string - projectId: - type: string - tags: - type: string - tagsAny: - type: string - type: object + detect existing IPv4 and/or IPv6 subnets. + items: + properties: + cidr: + type: string + description: + type: string + gateway_ip: + type: string + id: + type: string + ipVersion: + type: integer + ipv6AddressMode: + type: string + ipv6RaMode: + type: string + name: + type: string + notTags: + type: string + notTagsAny: + type: string + projectId: + type: string + tags: + type: string + tagsAny: + type: string + type: object + maxItems: 2 + type: array tags: description: Tags for all resources in cluster items: diff --git a/controllers/openstackcluster_controller.go b/controllers/openstackcluster_controller.go index e954631e1d..4c46020fcc 100644 --- a/controllers/openstackcluster_controller.go +++ b/controllers/openstackcluster_controller.go @@ -49,6 +49,7 @@ import ( "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/loadbalancer" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking" "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope" + utils "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/controllers" ) const ( @@ -516,26 +517,15 @@ func reconcileNetworkComponents(scope scope.Scope, cluster *clusterv1.Cluster, o openStackCluster.Status.Network.Name = networkList[0].Name openStackCluster.Status.Network.Tags = networkList[0].Tags - subnet, err := networkingService.GetNetworkSubnetByFilter(openStackCluster.Status.Network.ID, &openStackCluster.Spec.Subnet) + subnets, err := filterSubnets(networkingService, openStackCluster) if err != nil { - err = fmt.Errorf("failed to find subnet: %w", err) - - // Set the cluster to failed if subnet filter is invalid - if errors.Is(err, networking.ErrFilterMatch) { - handleUpdateOSCError(openStackCluster, err) - } - return err } - openStackCluster.Status.Network.Subnets = []infrav1.Subnet{ - { - ID: subnet.ID, - Name: subnet.Name, - CIDR: subnet.CIDR, - Tags: subnet.Tags, - }, + if err := utils.ValidateSubnets(subnets); err != nil { + return err } + openStackCluster.Status.Network.Subnets = subnets } else { err := networkingService.ReconcileNetwork(openStackCluster, clusterName) if err != nil { @@ -682,3 +672,45 @@ func handleUpdateOSCError(openstackCluster *infrav1.OpenStackCluster, message er openstackCluster.Status.FailureReason = &err openstackCluster.Status.FailureMessage = pointer.String(message.Error()) } + +// filterSubnets retrieves the subnets based on the Subnet filters specified on OpenstackCluster. +func filterSubnets(networkingService *networking.Service, openStackCluster *infrav1.OpenStackCluster) ([]infrav1.Subnet, error) { + var subnets []infrav1.Subnet + openStackClusterSubnets := openStackCluster.Spec.Subnets + if openStackCluster.Status.Network == nil { + return nil, nil + } + networkID := openStackCluster.Status.Network.ID + if len(openStackClusterSubnets) == 0 { + empty := &infrav1.SubnetFilter{} + listOpt := empty.ToListOpt() + listOpt.NetworkID = networkID + filteredSubnets, err := networkingService.GetSubnetsByFilter(listOpt) + if err != nil { + err = fmt.Errorf("failed to find subnets: %w", err) + if errors.Is(err, networking.ErrFilterMatch) { + handleUpdateOSCError(openStackCluster, err) + } + return nil, err + } + if len(filteredSubnets) > 2 { + return nil, fmt.Errorf("more than two subnets found in the Network. Specify the subnets in the OpenStackCluster.Spec instead") + } + for _, subnet := range filteredSubnets { + subnets = networkingService.ConvertOpenStackSubnetToCAPOSubnet(subnets, &subnet) + } + } else { + for subnet := range openStackClusterSubnets { + filteredSubnet, err := networkingService.GetNetworkSubnetByFilter(networkID, &openStackClusterSubnets[subnet]) + if err != nil { + err = fmt.Errorf("failed to find subnet: %w", err) + if errors.Is(err, networking.ErrFilterMatch) { + handleUpdateOSCError(openStackCluster, err) + } + return nil, err + } + subnets = networkingService.ConvertOpenStackSubnetToCAPOSubnet(subnets, filteredSubnet) + } + } + return subnets, nil +} diff --git a/controllers/openstackcluster_controller_test.go b/controllers/openstackcluster_controller_test.go index 5cf980b024..f45e5d8431 100644 --- a/controllers/openstackcluster_controller_test.go +++ b/controllers/openstackcluster_controller_test.go @@ -450,6 +450,74 @@ var _ = Describe("OpenStackCluster controller", func() { err = reconcileNetworkComponents(scope, capiCluster, testCluster) Expect(err).To(BeNil()) }) + + It("should allow two subnets for the cluster network", func() { + const externalNetworkID = "a42211a2-4d2c-426f-9413-830e4b4abbbc" + const clusterNetworkID = "6c90b532-7ba0-418a-a276-5ae55060b5b0" + clusterSubnets := []string{"cad5a91a-36de-4388-823b-b0cc82cadfdc", "e2407c18-c4e7-4d3d-befa-8eec5d8756f2"} + + testCluster.SetName("subnet-filtering") + testCluster.Spec = infrav1.OpenStackClusterSpec{ + DisableAPIServerFloatingIP: true, + APIServerFixedIP: "10.0.0.1", + ExternalNetwork: infrav1.NetworkFilter{ + ID: externalNetworkID, + }, + Network: infrav1.NetworkFilter{ + ID: clusterNetworkID, + }, + Subnets: []infrav1.SubnetFilter{ + {ID: clusterSubnets[0]}, + {ID: clusterSubnets[1]}, + }, + } + err := k8sClient.Create(ctx, testCluster) + Expect(err).To(BeNil()) + err = k8sClient.Create(ctx, capiCluster) + Expect(err).To(BeNil()) + scope, err := mockScopeFactory.NewClientScopeFromCluster(ctx, k8sClient, testCluster, nil, logr.Discard()) + Expect(err).To(BeNil()) + + networkClientRecorder := mockScopeFactory.NetworkClient.EXPECT() + + // Fetch external network + networkClientRecorder.ListNetwork(external.ListOptsExt{ + ListOptsBuilder: networks.ListOpts{ + ID: externalNetworkID, + }, + }).Return([]networks.Network{ + { + ID: externalNetworkID, + Name: "external-network", + }, + }, nil) + + // Fetch cluster network + networkClientRecorder.ListNetwork(&networks.ListOpts{ + ID: clusterNetworkID, + }).Return([]networks.Network{ + { + ID: clusterNetworkID, + Name: "cluster-network", + }, + }, nil) + + networkClientRecorder.GetSubnet(clusterSubnets[0]).Return(&subnets.Subnet{ + ID: clusterSubnets[0], + Name: "cluster-subnet", + CIDR: "192.168.0.0/24", + }, nil) + + networkClientRecorder.GetSubnet(clusterSubnets[1]).Return(&subnets.Subnet{ + ID: clusterSubnets[1], + Name: "cluster-subnet-v6", + CIDR: "2001:db8:2222:5555::/64", + }, nil) + + err = reconcileNetworkComponents(scope, capiCluster, testCluster) + Expect(err).To(BeNil()) + Expect(len(testCluster.Status.Network.Subnets)).To(Equal(2)) + }) }) func createRequestFromOSCluster(openStackCluster *infrav1.OpenStackCluster) reconcile.Request { diff --git a/pkg/cloud/services/networking/network.go b/pkg/cloud/services/networking/network.go index 601ac70a79..5eaf691471 100644 --- a/pkg/cloud/services/networking/network.go +++ b/pkg/cloud/services/networking/network.go @@ -383,3 +383,15 @@ func getSubnetName(clusterName string) string { func getNetworkName(clusterName string) string { return fmt.Sprintf("%s-cluster-%s", networkPrefix, clusterName) } + +// ConvertOpenStackSubnetToCAPOSubnet converts an OpenStack subnet to a capo subnet and adds to a slice. +// It returns the slice with the converted subnet. +func (s *Service) ConvertOpenStackSubnetToCAPOSubnet(subnets []infrav1.Subnet, filteredSubnet *subnets.Subnet) []infrav1.Subnet { + subnets = append(subnets, infrav1.Subnet{ + ID: filteredSubnet.ID, + Name: filteredSubnet.Name, + CIDR: filteredSubnet.CIDR, + Tags: filteredSubnet.Tags, + }) + return subnets +} diff --git a/pkg/utils/controllers/controllers.go b/pkg/utils/controllers/controllers.go new file mode 100644 index 0000000000..94dfb11ac4 --- /dev/null +++ b/pkg/utils/controllers/controllers.go @@ -0,0 +1,48 @@ +/* +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 controllers + +import ( + "fmt" + "net" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha8" +) + +// ValidateSubnets validates if the amount of IPv4 and IPv6 subnets is allowed by OpenStackCluster. +func ValidateSubnets(subnets []infrav1.Subnet) error { + isIPv6 := []bool{false, false} + for i, subnet := range subnets { + ip, _, err := net.ParseCIDR(subnet.CIDR) + if err != nil { + return err + } + + if ip.To4() == nil { + isIPv6[i] = true + } + } + + if len(subnets) > 1 && isIPv6[0] == isIPv6[1] { + ethertype := 4 + if isIPv6[0] { + ethertype = 6 + } + return fmt.Errorf("multiple IPv%d Subnet not allowed on OpenStackCluster", ethertype) + } + return nil +} diff --git a/pkg/utils/controllers/controllers_test.go b/pkg/utils/controllers/controllers_test.go new file mode 100644 index 0000000000..eadff95b77 --- /dev/null +++ b/pkg/utils/controllers/controllers_test.go @@ -0,0 +1,101 @@ +/* +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 controllers + +import ( + "testing" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha8" +) + +func Test_validateSubnets(t *testing.T) { + tests := []struct { + name string + subnets []infrav1.Subnet + wantErr bool + }{ + { + name: "valid IPv4 and IPv6 subnets", + subnets: []infrav1.Subnet{ + { + CIDR: "192.168.0.0/24", + }, + { + CIDR: "2001:db8:2222:5555::/64", + }, + }, + wantErr: false, + }, + { + name: "valid IPv4 and IPv6 subnets", + subnets: []infrav1.Subnet{ + { + CIDR: "2001:db8:2222:5555::/64", + }, + { + CIDR: "192.168.0.0/24", + }, + }, + wantErr: false, + }, + { + name: "multiple IPv4 subnets", + subnets: []infrav1.Subnet{ + { + CIDR: "192.168.0.0/24", + }, + { + CIDR: "10.0.0.0/24", + }, + }, + wantErr: true, + }, + { + name: "multiple IPv6 subnets", + subnets: []infrav1.Subnet{ + { + CIDR: "2001:db8:2222:5555::/64", + }, + { + CIDR: "2001:db8:2222:5555::/64", + }, + }, + wantErr: true, + }, + { + name: "invalid IP address", + subnets: []infrav1.Subnet{ + { + CIDR: "192.168.0.0/24", + }, + { + CIDR: "invalid", + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateSubnets(tt.subnets) + if (err != nil) != tt.wantErr { + t.Errorf("validateSubnets() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}