diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 735220825..2f3a2f853 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -158,6 +158,13 @@ type LoadBalancerSpec struct { // Region contains the name of the HCloud location the load balancer is running. Region Region `json:"region,omitempty"` + + // UseIPv6Endpoint defines whether to use the LoadBalancer's IPv6 address as + // the cluster endpoint instead of IPv4. This is useful if nodes are provisioned + // without IPv4 address. Defaults to 'false'. + // +optional + // +kubebuilder:default=false + UseIPv6Endpoint bool `json:"useIPv6Endpoint,omitempty"` } // LoadBalancerServiceSpec defines a Loadbalancer Target. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml index af5352dd0..892ba4c7f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclusters.yaml @@ -155,6 +155,13 @@ spec: - lb21 - lb31 type: string + useIPv6Endpoint: + default: false + description: UseIPv6Endpoint defines whether to use the LoadBalancer's + IPv6 address as the cluster endpoint instead of IPv4. This is + useful if nodes are provisioned without IPv4 address. Defaults + to 'false'. + type: boolean type: object controlPlaneRegions: description: ControlPlaneRegion consists of a list of HCloud Regions diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml index fa73cdc96..361800da1 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_hetznerclustertemplates.yaml @@ -177,6 +177,13 @@ spec: - lb21 - lb31 type: string + useIPv6Endpoint: + default: false + description: UseIPv6Endpoint defines whether to use the + LoadBalancer's IPv6 address as the cluster endpoint + instead of IPv4. This is useful if nodes are provisioned + without IPv4 address. Defaults to 'false'. + type: boolean type: object controlPlaneRegions: description: ControlPlaneRegion consists of a list of HCloud diff --git a/controllers/controllers_suite_test.go b/controllers/controllers_suite_test.go index c1002d207..9630409b4 100644 --- a/controllers/controllers_suite_test.go +++ b/controllers/controllers_suite_test.go @@ -154,9 +154,10 @@ func getDefaultHetznerClusterSpec() infrav1.HetznerClusterSpec { Protocol: "tcp", }, }, - Port: 6443, - Region: "fsn1", - Type: "lb11", + Port: 6443, + Region: "fsn1", + Type: "lb11", + UseIPv6Endpoint: false, }, ControlPlaneEndpoint: &clusterv1.APIEndpoint{}, ControlPlaneRegions: []infrav1.Region{"fsn1"}, diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go index d4ef150c7..a5d6ea312 100644 --- a/controllers/hetznercluster_controller.go +++ b/controllers/hetznercluster_controller.go @@ -243,8 +243,13 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS func processControlPlaneEndpoint(hetznerCluster *infrav1.HetznerCluster) { if hetznerCluster.Spec.ControlPlaneLoadBalancer.Enabled { - if hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 != "" { - defaultHost := hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 + ip := hetznerCluster.Status.ControlPlaneLoadBalancer.IPv4 + if hetznerCluster.Spec.ControlPlaneLoadBalancer.UseIPv6Endpoint { + ip = hetznerCluster.Status.ControlPlaneLoadBalancer.IPv6 + } + + if ip != "" { + defaultHost := ip defaultPort := int32(hetznerCluster.Spec.ControlPlaneLoadBalancer.Port) if hetznerCluster.Spec.ControlPlaneEndpoint == nil { diff --git a/controllers/hetznercluster_controller_test.go b/controllers/hetznercluster_controller_test.go index ab0528851..741faf058 100644 --- a/controllers/hetznercluster_controller_test.go +++ b/controllers/hetznercluster_controller_test.go @@ -1208,4 +1208,178 @@ func TestSetControlPlaneEndpoint(t *testing.T) { t.Fatalf("return value should be true") } }) + + t.Run("return false if load balancer is enabled with UseIPv6Endpoint and IPv6 is 'nil'. ControlPlaneEndpoint should not change", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + }, + ControlPlaneEndpoint: nil, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv4: "xyz", + IPv6: "", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint != nil { + t.Fatalf("ControlPlaneEndpoint should not change. It should remain nil") + } + + if hetznerCluster.Status.Ready != false { + t.Fatalf("return value should be false") + } + + if !conditions.Has(hetznerCluster, infrav1.ControlPlaneEndpointSetCondition) { + t.Fatalf("ControlPlaneEndpointSetCondition should exist") + } + + condition := conditions.Get(hetznerCluster, infrav1.ControlPlaneEndpointSetCondition) + if condition.Status != corev1.ConditionFalse { + t.Fatalf("condition status should be false") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil and ControlPlaneEndpoint is nil. Values of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port will get updated", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: nil, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 11 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 11", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endopint, IPv6 is not nil, ControlPlaneEndpoint.Host is an empty string and ControlPlaneEndpoint.Port is 0. Values of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port should update", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "", + Port: 0, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 11 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 11", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil, ControlPlaneEndpoint.Host is an empty string and ControlPlaneEndpoint.Port is 21. Value of ControlPlaneEndpoint.Host will change and ControlPlaneEndpoint.Port should remain same", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "", + Port: 21, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "abc" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'abc'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 21 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 21", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) + + t.Run("return true if load balancer is enabled with UseIPv6Endpoint, IPv6 is not nil, ControlPlaneEndpoint.Host is 'xyz' and ControlPlaneEndpoint.Port is 21. Value of ControlPlaneEndpoint.Host and ControlPlaneEndpoint.Port should remain unchanged", func(t *testing.T) { + hetznerCluster := &infrav1.HetznerCluster{ + Spec: infrav1.HetznerClusterSpec{ + ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{ + UseIPv6Endpoint: true, + Enabled: true, + Port: 11, + }, + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + Host: "xyz", + Port: 21, + }, + }, + Status: infrav1.HetznerClusterStatus{ + ControlPlaneLoadBalancer: &infrav1.LoadBalancerStatus{ + IPv6: "abc", + }, + }, + } + + processControlPlaneEndpoint(hetznerCluster) + + if hetznerCluster.Spec.ControlPlaneEndpoint.Host != "xyz" { + t.Fatalf("Wrong value for Host set. Got: %s, Want: 'xyz'", hetznerCluster.Spec.ControlPlaneEndpoint.Host) + } + + if hetznerCluster.Spec.ControlPlaneEndpoint.Port != 21 { + t.Fatalf("Wrong value for Port set. Got: %d, Want: 21", hetznerCluster.Spec.ControlPlaneEndpoint.Port) + } + + if hetznerCluster.Status.Ready != true { + t.Fatalf("return value should be true") + } + }) } diff --git a/hack/kind-dev.sh b/hack/kind-dev.sh index 36f7ab764..7aefcba69 100755 --- a/hack/kind-dev.sh +++ b/hack/kind-dev.sh @@ -45,8 +45,9 @@ kindV1Alpha4Cluster: - role: control-plane image: kindest/node:${CLUSTER_VERSION} networking: - podSubnet: "10.244.0.0/16" - serviceSubnet: "10.96.0.0/12" + podSubnet: "10.244.0.0/16,fd00:10:244::/56" + serviceSubnet: "10.96.0.0/12,fd00:10:96::/112" + ipFamily: dual EOF }