diff --git a/pkg/services/hcloud/network/network.go b/pkg/services/hcloud/network/network.go index b3614b7cb..e1d57698f 100644 --- a/pkg/services/hcloud/network/network.go +++ b/pkg/services/hcloud/network/network.go @@ -173,7 +173,14 @@ func (s *Service) findNetwork(ctx context.Context) (*hcloud.Network, error) { } if len(networks[0].Subnets) > 1 { - return nil, fmt.Errorf("multiple subnets not allowed") + configuredSubnet := s.scope.HetznerCluster.Spec.HCloudNetwork.SubnetCIDRBlock + firstSubnet := networks[0].Subnets[0] + + // Allow multiple subnets only if the first subnet matches the configured one. On attaching a server to a + // network the first subnet is used. + if firstSubnet.IPRange.String() != configuredSubnet { + return nil, fmt.Errorf("multiple subnets found and first subnet %s doesn't match the configured %s", firstSubnet.IPRange.String(), configuredSubnet) + } } return networks[0], nil diff --git a/pkg/services/hcloud/network/network_suite_test.go b/pkg/services/hcloud/network/network_suite_test.go new file mode 100644 index 000000000..0ebdc1e07 --- /dev/null +++ b/pkg/services/hcloud/network/network_suite_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2025 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 network + +import ( + "net" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1" + "github.com/syself/cluster-api-provider-hetzner/pkg/scope" + hcloudclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/client" + fakeclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/hcloud/client/fake" +) + +var ( + _, networkCidr, _ = net.ParseCIDR("10.0.0.0/16") + _, subnetCidr, _ = net.ParseCIDR("10.0.0.0/24") + hetznerCluster infrav1.HetznerCluster + service Service + hcloudClient hcloudclient.Client +) + +func TestNetwork(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Network Suite") +} + +var _ = BeforeSuite(func() { + hetznerCluster.Name = "hetzner-cluster" + hetznerCluster.Spec.HCloudNetwork = infrav1.HCloudNetworkSpec{ + Enabled: true, + CIDRBlock: networkCidr.String(), + SubnetCIDRBlock: subnetCidr.String(), + NetworkZone: "eu-central", + } + + hcloudClient = fakeclient.NewHCloudClientFactory().NewClient("") + service = Service{&scope.ClusterScope{HetznerCluster: &hetznerCluster, HCloudClient: hcloudClient}} +}) diff --git a/pkg/services/hcloud/network/network_test.go b/pkg/services/hcloud/network/network_test.go index 3bfcf61a9..612bbbac1 100644 --- a/pkg/services/hcloud/network/network_test.go +++ b/pkg/services/hcloud/network/network_test.go @@ -17,50 +17,26 @@ limitations under the License. package network import ( + "context" + "fmt" "net" - "testing" "github.com/hetznercloud/hcloud-go/v2/hcloud" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1" - "github.com/syself/cluster-api-provider-hetzner/pkg/scope" + "github.com/syself/cluster-api-provider-hetzner/pkg/utils" ) -func TestNetwork(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Network Suite") -} - var _ = Describe("Test createOpts", func() { - var hetznerCluster infrav1.HetznerCluster - var service Service - BeforeEach(func() { - hetznerCluster.Spec.HCloudNetwork = infrav1.HCloudNetworkSpec{ - Enabled: true, - CIDRBlock: "10.0.0.0/16", - SubnetCIDRBlock: "10.0.0.0/24", - NetworkZone: "eu-central", - } - hetznerCluster.Name = "hetzner-cluster" - - service = Service{&scope.ClusterScope{HetznerCluster: &hetznerCluster}} - }) It("Outputs the correct NetworkCreateOpts", func() { - _, network, err := net.ParseCIDR("10.0.0.0/16") - Expect(err).To(BeNil()) - - _, subnet, err := net.ParseCIDR("10.0.0.0/24") - Expect(err).To(BeNil()) - expectOpts := hcloud.NetworkCreateOpts{ Name: "hetzner-cluster", - IPRange: network, + IPRange: networkCidr, Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, Subnets: []hcloud.NetworkSubnet{ { - IPRange: subnet, + IPRange: subnetCidr, NetworkZone: hcloud.NetworkZoneEUCentral, Type: hcloud.NetworkSubnetTypeCloud, }, @@ -84,3 +60,120 @@ var _ = Describe("Test createOpts", func() { Expect(err).ToNot(BeNil()) }) }) + +var _ = Describe("Test findNetwork", func() { + _, subnet2Cidr, _ := net.ParseCIDR("10.0.1.0/24") + + BeforeEach(func() { + Expect(subnet2Cidr).ToNot(BeNil()) + hcloudClient.Reset() + }) + + It("outputs the correct network", func() { + _, createErr := hcloudClient.CreateNetwork(context.Background(), hcloud.NetworkCreateOpts{ + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + }) + Expect(createErr).To(BeNil()) + + expectedNetwork := &hcloud.Network{ + ID: 1, + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + } + + network, err := service.findNetwork(context.Background()) + Expect(err).To(BeNil()) + Expect(network).To(Equal(expectedNetwork)) + }) + + It("outputs no network/error if there is no network available", func() { + network, err := service.findNetwork(context.Background()) + Expect(err).To(BeNil()) + Expect(network).To(BeNil()) + }) + + It("outputs the correct network if there are multiple subnets but the first one matches the configured one on the HetznerCluster", func() { + _, createErr := hcloudClient.CreateNetwork(context.Background(), hcloud.NetworkCreateOpts{ + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + {IPRange: subnet2Cidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + }) + Expect(createErr).To(BeNil()) + + expectedNetwork := &hcloud.Network{ + ID: 1, + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + {IPRange: subnet2Cidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + } + + network, err := service.findNetwork(context.Background()) + Expect(err).To(BeNil()) + Expect(network).To(Equal(expectedNetwork)) + }) + + It("gives an error if there there are multiple subnet and the first one doesn't match the configure one on the HetznerCluster", func() { + _, createErr := hcloudClient.CreateNetwork(context.Background(), hcloud.NetworkCreateOpts{ + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnet2Cidr, Type: hcloud.NetworkSubnetTypeCloud}, + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + }) + Expect(createErr).To(BeNil()) + + network, err := service.findNetwork(context.Background()) + Expect(network).To(BeNil()) + Expect(err).To(Equal(fmt.Errorf("multiple subnets found and first subnet 10.0.1.0/24 doesn't match the configured 10.0.0.0/24"))) + }) + + It("gives an error if there are multiple networks with the same label", func() { + _, createErr1 := hcloudClient.CreateNetwork(context.Background(), hcloud.NetworkCreateOpts{ + Name: "test-network", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + }) + Expect(createErr1).To(BeNil()) + + _, createErr2 := hcloudClient.CreateNetwork(context.Background(), hcloud.NetworkCreateOpts{ + Name: "test-network2", + IPRange: networkCidr, + Subnets: []hcloud.NetworkSubnet{ + {IPRange: subnetCidr, Type: hcloud.NetworkSubnetTypeCloud}, + }, + Labels: map[string]string{"caph-cluster-hetzner-cluster": "owned"}, + }) + Expect(createErr2).To(BeNil()) + + _, err := service.findNetwork(context.Background()) + expectedOpts := hcloud.NetworkListOpts{ + ListOpts: hcloud.ListOpts{ + LabelSelector: utils.LabelsToLabelSelector(map[string]string{"caph-cluster-hetzner-cluster": "owned"}), + }, + } + Expect(err).To(Equal(fmt.Errorf("found multiple networks with opts %v - not allowed", expectedOpts))) + }) + +})