Repository: hetznercloud/hcloud-cloud-controller-manager (go)

Issue:
Disabling IPv6 for loadbalancers
Hello,
Is there an option "annotation/flag" to disable IPv6 Support ? 

I have upgraded CCM to v1.9.1 but the external-dns erroring to : the target A is not a valid record. As there is an open issue with external-dns regarding IPv6 support  kubernetes-sigs/external-dns/issues/2044 

I understand the feature was re-enabled #137 , but the ingress-nginx-controller is always publishing both IPv4 and IPv6 even with ConfigMap -> disable-ipv6-dns: "true" 

The only workaround I found is to go back to v1.8.1 where IPv6 is completely not supported. 

Pull request description:
Add option to disable IPv6 for load balancers
Enabling IPv6 by default in #137 broke clusters using external-dns. This
commit adds an option to disable IPv6 by setting the

    load-balancer.hetzner.cloud/ipv6-disabled

annotation to true.

Closes #191

Patch:
diff --git a/hcloud/load_balancers.go b/hcloud/load_balancers.go
index 3a96c0d6..544c3b43 100644
--- a/hcloud/load_balancers.go
+++ b/hcloud/load_balancers.go
@@ -59,14 +59,23 @@ func (l *loadBalancers) GetLoadBalancer(
 		}, true, nil
 	}
 
-	return &v1.LoadBalancerStatus{Ingress: []v1.LoadBalancerIngress{
+	ingresses := []v1.LoadBalancerIngress{
 		{
 			IP: lb.PublicNet.IPv4.IP.String(),
 		},
-		{
+	}
+
+	disableIPV6, err := annotation.LBIPv6Disabled.BoolFromService(service)
+	if err != nil && !errors.Is(err, annotation.ErrNotSet) {
+		return nil, false, fmt.Errorf("%s: %v", op, err)
+	}
+	if !disableIPV6 {
+		ingresses = append(ingresses, v1.LoadBalancerIngress{
 			IP: lb.PublicNet.IPv6.IP.String(),
-		},
-	}}, true, nil
+		})
+	}
+
+	return &v1.LoadBalancerStatus{Ingress: ingresses}, true, nil
 }
 
 func (l *loadBalancers) GetLoadBalancerName(ctx context.Context, clusterName string, service *v1.Service) string {
@@ -165,15 +174,17 @@ func (l *loadBalancers) EnsureLoadBalancer(
 	if err != nil && !errors.Is(err, annotation.ErrNotSet) {
 		return nil, fmt.Errorf("%s: %w", op, err)
 	}
+
 	if !disablePubNet {
-		ingress = append(ingress,
-			v1.LoadBalancerIngress{
-				IP: lb.PublicNet.IPv4.IP.String(),
-			},
-			v1.LoadBalancerIngress{
-				IP: lb.PublicNet.IPv6.IP.String(),
-			},
-		)
+		ingress = append(ingress, v1.LoadBalancerIngress{IP: lb.PublicNet.IPv4.IP.String()})
+
+		disableIPV6, err := annotation.LBIPv6Disabled.BoolFromService(svc)
+		if err != nil && !errors.Is(err, annotation.ErrNotSet) {
+			return nil, fmt.Errorf("%s: %v", op, err)
+		}
+		if !disableIPV6 {
+			ingress = append(ingress, v1.LoadBalancerIngress{IP: lb.PublicNet.IPv6.IP.String()})
+		}
 	}
 
 	disablePrivIngress, err := l.getDisablePrivateIngress(svc)
diff --git a/internal/annotation/load_balancer.go b/internal/annotation/load_balancer.go
index 83cae743..28cdb9a4 100644
--- a/internal/annotation/load_balancer.go
+++ b/internal/annotation/load_balancer.go
@@ -20,6 +20,13 @@ const (
 	// the backend. Read-only.
 	LBPublicIPv6 Name = "load-balancer.hetzner.cloud/ipv6"
 
+	// LBIPv6Disabled disables the use of IPv6 for the Load Balancer.
+	//
+	// Set this annotation if you use external-dns.
+	//
+	// Default: false
+	LBIPv6Disabled Name = "load-balancer.hetzner.cloud/ipv6-disabled"
+
 	// LBName is the name of the Load Balancer. The name will be visible in
 	// the Hetzner Cloud API console.
 	LBName Name = "load-balancer.hetzner.cloud/name"


Test patch:
diff --git a/hcloud/load_balancers_test.go b/hcloud/load_balancers_test.go
index 88612430..e7a4345d 100644
--- a/hcloud/load_balancers_test.go
+++ b/hcloud/load_balancers_test.go
@@ -14,6 +14,35 @@ import (
 
 func TestLoadBalancers_GetLoadBalancer(t *testing.T) {
 	tests := []LoadBalancerTestCase{
+		{
+			Name:       "get load balancer without host name IPv6 disabled",
+			ServiceUID: "1",
+			ServiceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBIPv6Disabled: true,
+			},
+			LB: &hcloud.LoadBalancer{
+				ID:   1,
+				Name: "no-host-name",
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					IPv4: hcloud.LoadBalancerPublicNetIPv4{IP: net.ParseIP("1.2.3.4")},
+				},
+			},
+			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
+				tt.LBOps.
+					On("GetByK8SServiceUID", tt.Ctx, tt.Service).
+					Return(tt.LB, nil)
+			},
+			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
+				status, exists, err := tt.LoadBalancers.GetLoadBalancer(tt.Ctx, tt.ClusterName, tt.Service)
+				assert.NoError(t, err)
+				assert.True(t, exists)
+
+				if !assert.Len(t, status.Ingress, 1) {
+					return
+				}
+				assert.Equal(t, tt.LB.PublicNet.IPv4.IP.String(), status.Ingress[0].IP)
+			},
+		},
 		{
 			Name:       "get load balancer without host name",
 			ServiceUID: "1",
@@ -132,6 +161,37 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				assert.EqualError(t, err, "hcloud/loadBalancers.EnsureLoadBalancer: test error")
 			},
 		},
+		{
+			Name:       "public network only no ipv6",
+			ServiceUID: "2",
+			ServiceAnnotations: map[annotation.Name]interface{}{
+				annotation.LBName:         "pub-net-only-no-ipv6",
+				annotation.LBIPv6Disabled: true,
+			},
+			LB: &hcloud.LoadBalancer{
+				ID:               1,
+				Name:             "pub-net-only-no-ipv6",
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "lb11"},
+				Location:         &hcloud.Location{Name: "nbg1", NetworkZone: hcloud.NetworkZoneEUCentral},
+				PublicNet: hcloud.LoadBalancerPublicNet{
+					Enabled: true,
+					IPv4:    hcloud.LoadBalancerPublicNetIPv4{IP: net.ParseIP("1.2.3.4")},
+				},
+			},
+			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
+				setupSuccessMocks(tt, "pub-net-only-no-ipv6")
+			},
+			Perform: func(t *testing.T, tt *LoadBalancerTestCase) {
+				expected := &v1.LoadBalancerStatus{
+					Ingress: []v1.LoadBalancerIngress{
+						{IP: tt.LB.PublicNet.IPv4.IP.String()},
+					},
+				}
+				lbStat, err := tt.LoadBalancers.EnsureLoadBalancer(tt.Ctx, tt.ClusterName, tt.Service, tt.Nodes)
+				assert.NoError(t, err)
+				assert.Equal(t, expected, lbStat)
+			},
+		},
 		{
 			Name:       "public network only",
 			ServiceUID: "2",
@@ -223,7 +283,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				PublicNet: hcloud.LoadBalancerPublicNet{
 					Enabled: true,
 					IPv4:    hcloud.LoadBalancerPublicNetIPv4{IP: net.ParseIP("1.2.3.4")},
-					// IPv6:    hcloud.LoadBalancerPublicNetIPv6{IP: net.ParseIP("fe80::1")},
+					IPv6:    hcloud.LoadBalancerPublicNetIPv6{IP: net.ParseIP("fe80::1")},
 				},
 				PrivateNet: []hcloud.LoadBalancerPrivateNet{
 					{
@@ -256,7 +316,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 			ServiceUID: "5",
 			ServiceAnnotations: map[annotation.Name]interface{}{
 				annotation.LBName:                  "with-priv-net-no-priv-ingress",
-				annotation.LBDisablePrivateIngress: "true",
+				annotation.LBDisablePrivateIngress: true,
 			},
 			LB: &hcloud.LoadBalancer{
 				ID:               1,
@@ -266,7 +326,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				PublicNet: hcloud.LoadBalancerPublicNet{
 					Enabled: true,
 					IPv4:    hcloud.LoadBalancerPublicNetIPv4{IP: net.ParseIP("1.2.3.4")},
-					// IPv6:    hcloud.LoadBalancerPublicNetIPv6{IP: net.ParseIP("fe80::1")},
+					IPv6:    hcloud.LoadBalancerPublicNetIPv6{IP: net.ParseIP("fe80::1")},
 				},
 				PrivateNet: []hcloud.LoadBalancerPrivateNet{
 					{