diff --git a/pkg/cloud/services/networking/network.go b/pkg/cloud/services/networking/network.go index 2dbafc3eb4..4483c37829 100644 --- a/pkg/cloud/services/networking/network.go +++ b/pkg/cloud/services/networking/network.go @@ -25,6 +25,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/external" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + "k8s.io/apimachinery/pkg/api/equality" "k8s.io/utils/ptr" infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" @@ -201,6 +202,10 @@ func (s *Service) ReconcileSubnet(openStackCluster *infrav1.OpenStackCluster, cl } else if len(subnetList) == 1 { subnet = &subnetList[0] s.scope.Logger().V(6).Info("Reusing existing subnet", "name", subnet.Name, "id", subnet.ID) + + if err := s.updateSubnetDNSNameservers(openStackCluster, subnet); err != nil { + return err + } } openStackCluster.Status.Network.Subnets = []infrav1.Subnet{ @@ -248,6 +253,39 @@ func (s *Service) createSubnet(openStackCluster *infrav1.OpenStackCluster, clust return subnet, nil } +// updateSubnetDNSNameservers updates the DNS nameservers for an existing subnet if they differ from the desired configuration. +func (s *Service) updateSubnetDNSNameservers(openStackCluster *infrav1.OpenStackCluster, subnet *subnets.Subnet) error { + // Picking the first managed subnet since we only support one for now + desiredNameservers := openStackCluster.Spec.ManagedSubnets[0].DNSNameservers + currentNameservers := subnet.DNSNameservers + + var needsUpdate bool + if len(desiredNameservers) != len(currentNameservers) { + needsUpdate = true + } else { + needsUpdate = !equality.Semantic.DeepEqual(currentNameservers, desiredNameservers) + } + + if needsUpdate { + s.scope.Logger().Info("Updating subnet DNS nameservers", "id", subnet.ID, "from", currentNameservers, "to", desiredNameservers) + + updateOpts := subnets.UpdateOpts{ + DNSNameservers: &desiredNameservers, + } + + updatedSubnet, err := s.client.UpdateSubnet(subnet.ID, updateOpts) + if err != nil { + record.Warnf(openStackCluster, "FailedUpdateSubnet", "Failed to update DNS nameservers for subnet %s: %v", subnet.ID, err) + return err + } + + *subnet = *updatedSubnet + record.Eventf(openStackCluster, "SuccessfulUpdateSubnet", "Updated DNS nameservers for subnet %s", subnet.ID) + } + + return nil +} + func (s *Service) getNetworkByName(networkName string) (networks.Network, error) { opts := networks.ListOpts{ Name: networkName, diff --git a/pkg/cloud/services/networking/network_test.go b/pkg/cloud/services/networking/network_test.go index 200ffc30d8..01f86191c7 100644 --- a/pkg/cloud/services/networking/network_test.go +++ b/pkg/cloud/services/networking/network_test.go @@ -39,6 +39,99 @@ const ( clusterResourceName = "test-cluster" ) +func Test_updateSubnetDNSNameservers(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + const subnetID = "subnet-123" + + tests := []struct { + name string + currentNameservers []string + desiredNameservers []string + expect func(m *mock.MockNetworkClientMockRecorder) + }{ + { + name: "no changes needed", + currentNameservers: []string{"8.8.8.8", "8.8.4.4"}, + desiredNameservers: []string{"8.8.8.8", "8.8.4.4"}, + expect: func(*mock.MockNetworkClientMockRecorder) {}, + }, + { + name: "different nameservers", + currentNameservers: []string{"8.8.8.8", "8.8.4.4"}, + desiredNameservers: []string{"1.1.1.1", "1.0.0.1"}, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m.UpdateSubnet(subnetID, subnets.UpdateOpts{ + DNSNameservers: &[]string{"1.1.1.1", "1.0.0.1"}, + }).Return(&subnets.Subnet{ + ID: subnetID, + DNSNameservers: []string{"1.1.1.1", "1.0.0.1"}, + }, nil) + }, + }, + { + name: "different count", + currentNameservers: []string{"8.8.8.8"}, + desiredNameservers: []string{"8.8.8.8", "8.8.4.4"}, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m.UpdateSubnet(subnetID, subnets.UpdateOpts{ + DNSNameservers: &[]string{"8.8.8.8", "8.8.4.4"}, + }).Return(&subnets.Subnet{ + ID: subnetID, + DNSNameservers: []string{"8.8.8.8", "8.8.4.4"}, + }, nil) + }, + }, + { + name: "same nameservers but different order", + currentNameservers: []string{"8.8.8.8", "8.8.4.4"}, + desiredNameservers: []string{"8.8.4.4", "8.8.8.8"}, + expect: func(m *mock.MockNetworkClientMockRecorder) { + m.UpdateSubnet(subnetID, subnets.UpdateOpts{ + DNSNameservers: &[]string{"8.8.4.4", "8.8.8.8"}, + }).Return(&subnets.Subnet{ + ID: subnetID, + DNSNameservers: []string{"8.8.4.4", "8.8.8.8"}, + }, nil) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + mockClient := mock.NewMockNetworkClient(mockCtrl) + tt.expect(mockClient.EXPECT()) + + cluster := &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + DNSNameservers: tt.desiredNameservers, + }, + }, + }, + } + subnet := &subnets.Subnet{ + ID: subnetID, + DNSNameservers: tt.currentNameservers, + } + + scopeFactory := scope.NewMockScopeFactory(mockCtrl, "") + log := testr.New(t) + s := Service{ + client: mockClient, + scope: scope.NewWithLogger(scopeFactory, log), + } + + err := s.updateSubnetDNSNameservers(cluster, subnet) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(subnet.DNSNameservers).To(Equal(tt.desiredNameservers)) + }) + } +} + func Test_ReconcileNetwork(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -460,7 +553,8 @@ func Test_ReconcileSubnet(t *testing.T) { fakeSubnetID := "d08803fc-2fa5-4179-b9d7-8c43d0af2fe6" fakeCIDR := "10.0.0.0/24" fakeNetworkID := "d08803fc-2fa5-4279-b9f7-8c45d0ff2fe6" - fakeDNS := "10.0.10.200" + fakeDNS1 := "10.0.10.200" + fakeDNS2 := "10.0.10.201" tests := []struct { name string @@ -571,7 +665,7 @@ func Test_ReconcileSubnet(t *testing.T) { ManagedSubnets: []infrav1.SubnetSpec{ { CIDR: fakeCIDR, - DNSNameservers: []string{fakeDNS}, + DNSNameservers: []string{fakeDNS1}, }, }, }, @@ -595,7 +689,7 @@ func Test_ReconcileSubnet(t *testing.T) { IPVersion: 4, CIDR: fakeCIDR, Description: expectedSubnetDesc, - DNSNameservers: []string{fakeDNS}, + DNSNameservers: []string{fakeDNS1}, }). Return(&subnets.Subnet{ ID: fakeSubnetID, @@ -690,6 +784,288 @@ func Test_ReconcileSubnet(t *testing.T) { }, }, }, + { + name: "existing subnet with different DNS nameservers - update needed", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1}, + }, + }, + }, + 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, + DNSNameservers: []string{fakeDNS2}, + }, + }, nil) + + updateOpts := subnets.UpdateOpts{ + DNSNameservers: &[]string{fakeDNS1}, + } + + m.UpdateSubnet(fakeSubnetID, updateOpts). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1}, + }, nil). + Times(1) + }, + want: &infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + { + name: "existing subnet with same DNS nameservers - no update needed", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1}, + }, + }, + }, + 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, + DNSNameservers: []string{fakeDNS1}, + }, + }, nil) + }, + want: &infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + { + name: "existing subnet with multiple different DNS nameservers - update needed", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1, fakeDNS2}, + }, + }, + }, + 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, + DNSNameservers: []string{fakeDNS1}, + }, + }, nil) + + updateOpts := subnets.UpdateOpts{ + DNSNameservers: &[]string{fakeDNS1, fakeDNS2}, + } + + m.UpdateSubnet(fakeSubnetID, updateOpts). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1, fakeDNS2}, + }, nil). + Times(1) + }, + want: &infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + { + name: "existing subnet with multiple (invert) different DNS nameservers - update needed", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS2, fakeDNS1}, + }, + }, + }, + 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, + DNSNameservers: []string{fakeDNS2}, + }, + }, nil) + + updateOpts := subnets.UpdateOpts{ + DNSNameservers: &[]string{fakeDNS2, fakeDNS1}, + } + + m.UpdateSubnet(fakeSubnetID, updateOpts). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS2, fakeDNS1}, + }, nil). + Times(1) + }, + want: &infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, + { + name: "existing subnet with no DNS nameservers initially - add DNS nameservers", + openStackCluster: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1}, + }, + }, + }, + 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, + DNSNameservers: []string{}, + }, + }, nil) + + updateOpts := subnets.UpdateOpts{ + DNSNameservers: &[]string{fakeDNS1}, + } + + m.UpdateSubnet(fakeSubnetID, updateOpts). + Return(&subnets.Subnet{ + ID: fakeSubnetID, + Name: expectedSubnetName, + CIDR: fakeCIDR, + DNSNameservers: []string{fakeDNS1}, + }, nil). + Times(1) + }, + want: &infrav1.OpenStackClusterStatus{ + Network: &infrav1.NetworkStatusWithSubnets{ + NetworkStatus: infrav1.NetworkStatus{ + ID: fakeNetworkID, + }, + Subnets: []infrav1.Subnet{ + { + Name: expectedSubnetName, + ID: fakeSubnetID, + CIDR: fakeCIDR, + }, + }, + }, + }, + }, } for _, tt := range tests { diff --git a/pkg/webhooks/openstackcluster_webhook.go b/pkg/webhooks/openstackcluster_webhook.go index 19a2571b2a..a237572a8e 100644 --- a/pkg/webhooks/openstackcluster_webhook.go +++ b/pkg/webhooks/openstackcluster_webhook.go @@ -127,7 +127,7 @@ func allowSubnetFilterToIDTransition(oldObj, newObj *infrav1.OpenStackCluster) b } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. -func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, newObjRaw runtime.Object) (admission.Warnings, error) { +func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, newObjRaw runtime.Object) (admission.Warnings, error) { //nolint:gocyclo,cyclop var allErrs field.ErrorList oldObj, err := castToOpenStackCluster(oldObjRaw) if err != nil { @@ -193,6 +193,40 @@ func (*openStackClusterWebhook) ValidateUpdate(_ context.Context, oldObjRaw, new newObj.Spec.ManagedSecurityGroups.AllowAllInClusterTraffic = false } + // Allow changes only to DNSNameservers in ManagedSubnets spec + if newObj.Spec.ManagedSubnets != nil && oldObj.Spec.ManagedSubnets != nil { + // Check if any fields other than DNSNameservers have changed + if len(oldObj.Spec.ManagedSubnets) != len(newObj.Spec.ManagedSubnets) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "managedSubnets"), "cannot add or remove subnets")) + } else { + // Build maps of subnets by CIDR + oldSubnetMap := make(map[string]*infrav1.SubnetSpec) + + for i := range oldObj.Spec.ManagedSubnets { + oldSubnet := &oldObj.Spec.ManagedSubnets[i] + oldSubnetMap[oldSubnet.CIDR] = oldSubnet + } + + // Check if all new subnets have matching old subnets with the same CIDR + for i := range newObj.Spec.ManagedSubnets { + newSubnet := &newObj.Spec.ManagedSubnets[i] + + oldSubnet, exists := oldSubnetMap[newSubnet.CIDR] + if !exists { + allErrs = append(allErrs, field.Forbidden( + field.NewPath("spec", "managedSubnets"), + fmt.Sprintf("cannot change subnet CIDR from existing value to %s", newSubnet.CIDR), + )) + continue + } + + // DNSNameservers is mutable + oldSubnet.DNSNameservers = nil + newSubnet.DNSNameservers = nil + } + } + } + // Allow changes on AllowedCIDRs if newObj.Spec.APIServerLoadBalancer != nil && oldObj.Spec.APIServerLoadBalancer != nil { oldObj.Spec.APIServerLoadBalancer.AllowedCIDRs = []string{} diff --git a/pkg/webhooks/openstackcluster_webhook_test.go b/pkg/webhooks/openstackcluster_webhook_test.go index 7c8b8526b5..7ff7bcdba1 100644 --- a/pkg/webhooks/openstackcluster_webhook_test.go +++ b/pkg/webhooks/openstackcluster_webhook_test.go @@ -902,6 +902,441 @@ func TestOpenStackCluster_ValidateUpdate(t *testing.T) { }, wantErr: true, }, + + { + name: "Changing OpenStackCluster.Spec.ManagedSubnets.DNSNameservers is allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + "8.8.4.4", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "1.1.1.1", + "1.0.0.1", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Adding new DNSNameserver to OpenStackCluster.Spec.ManagedSubnets.DNSNameservers is allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + "8.8.4.4", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Removing DNSNameservers from OpenStackCluster.Spec.ManagedSubnets is allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + "8.8.4.4", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{}, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Multiple subnets with DNSNameservers changes are allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + { + CIDR: "192.168.2.0/24", + DNSNameservers: []string{ + "8.8.4.4", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.2.10", + End: "192.168.2.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "1.1.1.1", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + { + CIDR: "192.168.2.0/24", + DNSNameservers: []string{ + "1.0.0.1", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.2.10", + End: "192.168.2.100", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Changing CIDR in OpenStackCluster.Spec.ManagedSubnets is not allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "10.0.0.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "10.0.0.10", + End: "10.0.0.100", + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "Modifying AllocationPools in OpenStackCluster.Spec.ManagedSubnets is not allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.20", + End: "192.168.1.200", + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "Adding a new subnet to OpenStackCluster.Spec.ManagedSubnets is not allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + { + CIDR: "192.168.2.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.2.10", + End: "192.168.2.100", + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "Removing a subnet from OpenStackCluster.Spec.ManagedSubnets is not allowed", + oldTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + { + CIDR: "192.168.2.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.2.10", + End: "192.168.2.100", + }, + }, + }, + }, + }, + }, + newTemplate: &infrav1.OpenStackCluster{ + Spec: infrav1.OpenStackClusterSpec{ + IdentityRef: infrav1.OpenStackIdentityReference{ + Name: "foobar", + CloudName: "foobar", + }, + ManagedSubnets: []infrav1.SubnetSpec{ + { + CIDR: "192.168.1.0/24", + DNSNameservers: []string{ + "8.8.8.8", + }, + AllocationPools: []infrav1.AllocationPool{ + { + Start: "192.168.1.10", + End: "192.168.1.100", + }, + }, + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {