diff --git a/api/v1alpha5/zz_generated.conversion.go b/api/v1alpha5/zz_generated.conversion.go index b10bd71be9..203890c08d 100644 --- a/api/v1alpha5/zz_generated.conversion.go +++ b/api/v1alpha5/zz_generated.conversion.go @@ -709,6 +709,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha5_OpenStackClusterSpec( // 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)) + // WARNING: in.AllocationPools requires manual conversion: does not exist in peer-type if in.ExternalRouterIPs != nil { in, out := &in.ExternalRouterIPs, &out.ExternalRouterIPs *out = make([]ExternalRouterIPParam, len(*in)) diff --git a/api/v1alpha6/conversion.go b/api/v1alpha6/conversion.go index c2170b980b..5a7e27192c 100644 --- a/api/v1alpha6/conversion.go +++ b/api/v1alpha6/conversion.go @@ -154,6 +154,11 @@ var v1alpha8OpenStackClusterRestorer = conversion.RestorerFor[*infrav1.OpenStack return &c.Spec.NetworkMTU }, ), + "allocationPools": conversion.UnconditionalFieldRestorer( + func(c *infrav1.OpenStackCluster) *[]infrav1.AllocationPool { + return &c.Spec.AllocationPools + }, + ), "bastion": conversion.HashedFieldRestorer( func(c *infrav1.OpenStackCluster) **infrav1.Bastion { return &c.Spec.Bastion @@ -240,6 +245,11 @@ var v1alpha8OpenStackClusterTemplateRestorer = conversion.RestorerFor[*infrav1.O return &c.Spec.Template.Spec.NetworkMTU }, ), + "allocationPools": conversion.UnconditionalFieldRestorer( + func(c *infrav1.OpenStackClusterTemplate) *[]infrav1.AllocationPool { + return &c.Spec.Template.Spec.AllocationPools + }, + ), "bastion": conversion.HashedFieldRestorer( func(c *infrav1.OpenStackClusterTemplate) **infrav1.Bastion { return &c.Spec.Template.Spec.Bastion diff --git a/api/v1alpha6/zz_generated.conversion.go b/api/v1alpha6/zz_generated.conversion.go index 41761e8db2..f1dbc4b05c 100644 --- a/api/v1alpha6/zz_generated.conversion.go +++ b/api/v1alpha6/zz_generated.conversion.go @@ -732,6 +732,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha6_OpenStackClusterSpec( // 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)) + // WARNING: in.AllocationPools requires manual conversion: does not exist in peer-type if in.ExternalRouterIPs != nil { in, out := &in.ExternalRouterIPs, &out.ExternalRouterIPs *out = make([]ExternalRouterIPParam, len(*in)) diff --git a/api/v1alpha7/conversion.go b/api/v1alpha7/conversion.go index e51c698e29..773f17846b 100644 --- a/api/v1alpha7/conversion.go +++ b/api/v1alpha7/conversion.go @@ -98,6 +98,8 @@ func restorev1alpha8ClusterSpec(previous *infrav1.OpenStackClusterSpec, dst *inf dst.DisableExternalNetwork = previous.DisableExternalNetwork + dst.AllocationPools = previous.AllocationPools + if len(previous.Subnets) > 1 { dst.Subnets = append(dst.Subnets, previous.Subnets[1:]...) } diff --git a/api/v1alpha7/zz_generated.conversion.go b/api/v1alpha7/zz_generated.conversion.go index 773d9892fd..5573dd38b3 100644 --- a/api/v1alpha7/zz_generated.conversion.go +++ b/api/v1alpha7/zz_generated.conversion.go @@ -924,6 +924,7 @@ func autoConvert_v1alpha8_OpenStackClusterSpec_To_v1alpha7_OpenStackClusterSpec( // WARNING: in.Subnets requires manual conversion: does not exist in peer-type out.NetworkMTU = in.NetworkMTU out.DNSNameservers = *(*[]string)(unsafe.Pointer(&in.DNSNameservers)) + // WARNING: in.AllocationPools requires manual conversion: does not exist in peer-type out.ExternalRouterIPs = *(*[]ExternalRouterIPParam)(unsafe.Pointer(&in.ExternalRouterIPs)) // WARNING: in.ExternalNetwork requires manual conversion: does not exist in peer-type // WARNING: in.DisableExternalNetwork requires manual conversion: does not exist in peer-type diff --git a/api/v1alpha8/openstackcluster_types.go b/api/v1alpha8/openstackcluster_types.go index 7430fab1ab..2f3ea07a70 100644 --- a/api/v1alpha8/openstackcluster_types.go +++ b/api/v1alpha8/openstackcluster_types.go @@ -63,6 +63,12 @@ type OpenStackClusterSpec struct { // through DNS is required. // +listType=set DNSNameservers []string `json:"dnsNameservers,omitempty"` + + // AllocationPools is an array of AllocationPool objects that will be applied to OpenStack Subnet being created. + // If set, OpenStack will only allocate these IPs for Machines. It will still be possible to create ports from + // outside of these ranges manually. + AllocationPools []AllocationPool `json:"allocationPools,omitempty"` + // ExternalRouterIPs is an array of externalIPs on the respective subnets. // This is necessary if the router needs a fixed ip in a specific subnet. ExternalRouterIPs []ExternalRouterIPParam `json:"externalRouterIPs,omitempty"` diff --git a/api/v1alpha8/types.go b/api/v1alpha8/types.go index bdbf2155bf..158af29d7b 100644 --- a/api/v1alpha8/types.go +++ b/api/v1alpha8/types.go @@ -87,6 +87,11 @@ type RouterFilter struct { NotTagsAny string `json:"notTagsAny,omitempty"` } +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + type PortOpts struct { // Network is a query for an openstack network that the port will be created or discovered on. // This will fail if the query returns more than one network. diff --git a/api/v1alpha8/zz_generated.deepcopy.go b/api/v1alpha8/zz_generated.deepcopy.go index eebb59993e..fec9d3ebb7 100644 --- a/api/v1alpha8/zz_generated.deepcopy.go +++ b/api/v1alpha8/zz_generated.deepcopy.go @@ -83,6 +83,21 @@ func (in *AddressPair) DeepCopy() *AddressPair { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AllocationPool) DeepCopyInto(out *AllocationPool) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AllocationPool. +func (in *AllocationPool) DeepCopy() *AllocationPool { + if in == nil { + return nil + } + out := new(AllocationPool) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Bastion) DeepCopyInto(out *Bastion) { *out = *in @@ -382,6 +397,11 @@ func (in *OpenStackClusterSpec) DeepCopyInto(out *OpenStackClusterSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AllocationPools != nil { + in, out := &in.AllocationPools, &out.AllocationPools + *out = make([]AllocationPool, len(*in)) + copy(*out, *in) + } if in.ExternalRouterIPs != nil { in, out := &in.ExternalRouterIPs, &out.ExternalRouterIPs *out = make([]ExternalRouterIPParam, 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 00b1ba6cc9..030d96cc29 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclusters.yaml @@ -4838,6 +4838,22 @@ spec: spec: description: OpenStackClusterSpec defines the desired state of OpenStackCluster. properties: + allocationPools: + description: |- + AllocationPools is an array of AllocationPool objects that will be applied to OpenStack Subnet being created. + If set, OpenStack will only allocate these IPs for Machines. It will still be possible to create ports from + outside of these ranges manually. + items: + properties: + end: + type: string + start: + type: string + required: + - end + - start + type: object + type: array allowAllInClusterTraffic: description: |- AllowAllInClusterTraffic is only used when managed security groups are in use. 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 cb56c31021..c1b1c5c81a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_openstackclustertemplates.yaml @@ -2263,6 +2263,22 @@ spec: description: OpenStackClusterSpec defines the desired state of OpenStackCluster. properties: + allocationPools: + description: |- + AllocationPools is an array of AllocationPool objects that will be applied to OpenStack Subnet being created. + If set, OpenStack will only allocate these IPs for Machines. It will still be possible to create ports from + outside of these ranges manually. + items: + properties: + end: + type: string + start: + type: string + required: + - end + - start + type: object + type: array allowAllInClusterTraffic: description: |- AllowAllInClusterTraffic is only used when managed security groups are in use. diff --git a/docs/book/src/topics/crd-changes/v1alpha7-to-v1alpha8.md b/docs/book/src/topics/crd-changes/v1alpha7-to-v1alpha8.md index efcf44d169..366d1928b9 100644 --- a/docs/book/src/topics/crd-changes/v1alpha7-to-v1alpha8.md +++ b/docs/book/src/topics/crd-changes/v1alpha7-to-v1alpha8.md @@ -176,4 +176,8 @@ In v1alpha8, this will be automatically converted to: `Subnets` allows specifications of maximum two `SubnetFilter` one being IPv4 and the other IPv6. Both subnets must be on the same network. Any filtered subnets will be added to `OpenStackCluster.Status.Network.Subnets`. -When subnets are not specified on `OpenStackCluster` and only the network is, the network is used to identify the subnets to use. If more than two subnets exist in the network, the user must specify which ones to use by defining the `OpenStackCluster.Spec.Subnets` field. \ No newline at end of file +When subnets are not specified on `OpenStackCluster` and only the network is, the network is used to identify the subnets to use. If more than two subnets exist in the network, the user must specify which ones to use by defining the `OpenStackCluster.Spec.Subnets` field. + +#### Addition of allocationPools + +In v1alpha8, an `AllocationPools` property is introduced to `OpenStackClusterSpec`. When specified, OpenStack subnet created by CAPO will have the given values set as the `allocation_pools` property. This allows users to make sure OpenStack will not allocate some IP ranges in the subnet automatically. If the subnet is precreated and configured, CAPO will ignore `AllocationPools` property. \ No newline at end of file diff --git a/pkg/cloud/services/networking/network.go b/pkg/cloud/services/networking/network.go index 5eaf691471..a102826ebe 100644 --- a/pkg/cloud/services/networking/network.go +++ b/pkg/cloud/services/networking/network.go @@ -254,6 +254,10 @@ func (s *Service) createSubnet(openStackCluster *infrav1.OpenStackCluster, clust Description: names.GetDescription(clusterName), } + for _, pool := range openStackCluster.Spec.AllocationPools { + opts.AllocationPools = append(opts.AllocationPools, subnets.AllocationPool{Start: pool.Start, End: pool.End}) + } + subnet, err := s.client.CreateSubnet(opts) if err != nil { record.Warnf(openStackCluster, "FailedCreateSubnet", "Failed to create subnet %s: %v", name, err) diff --git a/pkg/cloud/services/networking/network_test.go b/pkg/cloud/services/networking/network_test.go index 512cc53146..1e4a8b6384 100644 --- a/pkg/cloud/services/networking/network_test.go +++ b/pkg/cloud/services/networking/network_test.go @@ -18,6 +18,7 @@ package networking import ( "reflect" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/names" "testing" "github.com/go-logr/logr" @@ -423,6 +424,244 @@ func Test_ReconcileExternalNetwork(t *testing.T) { } } +func Test_ReconcileSubnet(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + clusterName := "test-cluster" + expectedSubnetName := getSubnetName(clusterName) + expectedSubnetDesc := names.GetDescription(clusterName) + fakeSubnetID := "d08803fc-2fa5-4179-b9f7-8c43d0af2fe6" + fakeCIDR := "10.0.0.0/24" + fakeNetworkID := "d08803fc-2fa5-4279-b9f7-8c45d0af2fe6" + fakeDNS := "10.0.10.200" + + tests := []struct { + name string + openStackCluster *infrav1.OpenStackCluster + expect func(m *mock.MockNetworkClientMockRecorder) + want *infrav1.OpenStackCluster + }{ + { + name: "ensures status set when reconciling an existing subnet", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + NodeCIDR: fakeCIDR, + }, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + }, + }, + }, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m. + ListSubnet(subnets.ListOpts{NetworkID: fakeNetworkID, CIDR: fakeCIDR}). + Return([]subnets.Subnet{ + { + ID: fakeSubnetID, + Name: expectedSubnetName, + CIDR: fakeCIDR, + }, + }, nil) + }, + want: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{}, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + }, + { + name: "creation without any parameter", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + NodeCIDR: fakeCIDR, + }, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + }, + }, + }, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m. + ListSubnet(subnets.ListOpts{NetworkID: fakeNetworkID, CIDR: fakeCIDR}). + Return([]subnets.Subnet{}, nil) + + m. + CreateSubnet(subnets.CreateOpts{ + NetworkID: fakeNetworkID, + Name: expectedSubnetName, + IPVersion: 4, + CIDR: fakeCIDR, + Description: expectedSubnetDesc, + }). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + }, nil) + }, + want: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{}, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + }, + { + name: "creation with DNSNameservers", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + NodeCIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS}, + }, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + }, + }, + }, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m. + ListSubnet(subnets.ListOpts{NetworkID: fakeNetworkID, CIDR: fakeCIDR}). + Return([]subnets.Subnet{}, nil) + + m. + CreateSubnet(subnets.CreateOpts{ + NetworkID: fakeNetworkID, + Name: expectedSubnetName, + IPVersion: 4, + CIDR: fakeCIDR, + Description: expectedSubnetDesc, + DNSNameservers: []string{fakeDNS}, + }). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + }, nil) + }, + want: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{}, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + }, + { + name: "creation with allocationPools", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + NodeCIDR: fakeCIDR, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "10.0.0.1", + End: "10.0.0.10", + }, + { + Start: "10.0.0.20", + End: "10.0.0.254", + }, + }, + }, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + }, + }, + }, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m. + ListSubnet(subnets.ListOpts{NetworkID: fakeNetworkID, CIDR: fakeCIDR}). + Return([]subnets.Subnet{}, nil) + + m. + CreateSubnet(subnets.CreateOpts{ + NetworkID: fakeNetworkID, + Name: expectedSubnetName, + IPVersion: 4, + CIDR: fakeCIDR, + Description: expectedSubnetDesc, + AllocationPools: []subnets.AllocationPool{ + { + Start: "10.0.0.1", + End: "10.0.0.10", + }, + { + Start: "10.0.0.20", + End: "10.0.0.254", + }, + }, + }). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + }, nil) + }, + want: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{}, + Status: infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + mockClient := mock.NewMockNetworkClient(mockCtrl) + tt.expect(mockClient.EXPECT()) + s := Service{ + client: mockClient, + scope: scope.NewMockScopeFactory(mockCtrl, "", logr.Discard()), + } + err := s.ReconcileSubnet(tt.openStackCluster, clusterName) + g.Expect(err).ShouldNot(HaveOccurred()) + }) + } +} + func Test_ConvertOpenStackSubnetToCAPOSubnet(t *testing.T) { caposubnets := []infrav1.Subnet{ {