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

Issue:
Support IPv6 for loadbalancers
In the code for the load balancers, there are multiple lines that would indicate IPv6 support is ready and „just needs to be enabled“.

Those are:

* [getting the status](https://github.com/hetznercloud/hcloud-cloud-controller-manager/blob/be4b286969da99eade775af99452d6b87a9a5f73/hcloud/load_balancers.go#L147-L149)
* [setting the IPs](https://github.com/hetznercloud/hcloud-cloud-controller-manager/blob/be4b286969da99eade775af99452d6b87a9a5f73/hcloud/load_balancers.go#L58-L60)

Could you clarify the reason why the lines are commented? As far as I know, the upstream implementation supports IPv6 addresses on Ingresses.

Once I understand the issue, I’ll gladly extend the docs/the comment to clarify this for the next person.

Pull request description:
Re-enable IPv6 support
This commit re-enables the IPv6 support for Load Balancers managed by
the Cloud Controller Manager. From this commit on Load Balancers are
always created with an IPv4 and an IPv6 address.

Closes #121 

This MR is from @fhofherr, i just have the honor to push it to Github and merge it :)

Patch:
diff --git a/hcloud/load_balancers.go b/hcloud/load_balancers.go
index fce0a9e7..1e7dfb48 100644
--- a/hcloud/load_balancers.go
+++ b/hcloud/load_balancers.go
@@ -58,9 +58,9 @@ func (l *loadBalancers) GetLoadBalancer(
 		{
 			IP: lb.PublicNet.IPv4.IP.String(),
 		},
-		// {
-		// 	IP: loadBalancer.PublicNet.IPv6.IP.String(),
-		// },
+		{
+			IP: lb.PublicNet.IPv6.IP.String(),
+		},
 	}}, true, nil
 }
 
@@ -165,9 +165,9 @@ func (l *loadBalancers) EnsureLoadBalancer(
 			v1.LoadBalancerIngress{
 				IP: lb.PublicNet.IPv4.IP.String(),
 			},
-		// v1.LoadBalancerIngress{
-		// 	IP: loadBalancer.PublicNet.IPv6.IP.String(),
-		// }
+			v1.LoadBalancerIngress{
+				IP: lb.PublicNet.IPv6.IP.String(),
+			},
 		)
 	}
 


Test patch:
diff --git a/e2etests/e2e_test.go b/e2etests/e2e_test.go
index 2727afd3..bd12ac8f 100644
--- a/e2etests/e2e_test.go
+++ b/e2etests/e2e_test.go
@@ -58,15 +58,15 @@ func TestCloudControllerManagerPodIsPresent(t *testing.T) {
 }
 
 func TestCloudControllerManagerSetCorrectNodeLabelsAndIPAddresses(t *testing.T) {
-	node, err := testCluster.k8sClient.CoreV1().Nodes().Get(context.Background(), testCluster.setup.server.Name, metav1.GetOptions{})
+	node, err := testCluster.k8sClient.CoreV1().Nodes().Get(context.Background(), testCluster.setup.ClusterNode.Name, metav1.GetOptions{})
 	assert.NoError(t, err)
 
 	labels := node.Labels
 	expectedLabels := map[string]string{
-		"node.kubernetes.io/instance-type": testCluster.setup.server.ServerType.Name,
-		"topology.kubernetes.io/region":    testCluster.setup.server.Datacenter.Location.Name,
-		"topology.kubernetes.io/zone":      testCluster.setup.server.Datacenter.Name,
-		"kubernetes.io/hostname":           testCluster.setup.server.Name,
+		"node.kubernetes.io/instance-type": testCluster.setup.ClusterNode.ServerType.Name,
+		"topology.kubernetes.io/region":    testCluster.setup.ClusterNode.Datacenter.Location.Name,
+		"topology.kubernetes.io/zone":      testCluster.setup.ClusterNode.Datacenter.Name,
+		"kubernetes.io/hostname":           testCluster.setup.ClusterNode.Name,
 		"kubernetes.io/os":                 "linux",
 		"kubernetes.io/arch":               "amd64",
 	}
@@ -79,7 +79,7 @@ func TestCloudControllerManagerSetCorrectNodeLabelsAndIPAddresses(t *testing.T)
 	for _, address := range node.Status.Addresses {
 		switch address.Type {
 		case typesv1.NodeExternalIP:
-			expectedIP := testCluster.setup.server.PublicNet.IPv4.IP.String()
+			expectedIP := testCluster.setup.ClusterNode.PublicNet.IPv4.IP.String()
 			if expectedIP != address.Address {
 				t.Errorf("Got %s as NodeExternalIP but expected %s", address.Address, expectedIP)
 			}
@@ -89,7 +89,7 @@ func TestCloudControllerManagerSetCorrectNodeLabelsAndIPAddresses(t *testing.T)
 		for _, address := range node.Status.Addresses {
 			switch address.Type {
 			case typesv1.NodeInternalIP:
-				expectedIP := testCluster.setup.server.PrivateNet[0].IP.String()
+				expectedIP := testCluster.setup.ClusterNode.PrivateNet[0].IP.String()
 				if expectedIP != address.Address {
 					t.Errorf("Got %s as NodeInternalIP but expected %s", address.Address, expectedIP)
 				}
@@ -115,12 +115,22 @@ func TestCloudControllerManagerLoadBalancersMinimalSetup(t *testing.T) {
 	ingressIP := lbSvc.Status.LoadBalancer.Ingress[0].IP // Index 0 is always the public IP of the LB
 	WaitForHTTPAvailable(t, ingressIP, false)
 
+	for _, ing := range lbSvc.Status.LoadBalancer.Ingress {
+		WaitForHTTPOnServer(t, testCluster.setup.ExtServer, testCluster.setup.privKey, ing.IP, false)
+	}
+
 	lbTest.TearDown()
 }
 
 func TestCloudControllerManagerLoadBalancersHTTPS(t *testing.T) {
 	cert := testCluster.CreateTLSCertificate(t, "loadbalancer-https")
-	lbTest := lbTestHelper{t: t, K8sClient: testCluster.k8sClient, podName: "loadbalancer-https", port: 443}
+	lbTest := lbTestHelper{
+		t:             t,
+		K8sClient:     testCluster.k8sClient,
+		KeepOnFailure: testCluster.KeepOnFailure,
+		podName:       "loadbalancer-https",
+		port:          443,
+	}
 
 	pod := lbTest.DeployTestPod()
 
@@ -138,6 +148,10 @@ func TestCloudControllerManagerLoadBalancersHTTPS(t *testing.T) {
 	ingressIP := lbSvc.Status.LoadBalancer.Ingress[0].IP // Index 0 is always the public IP of the LB
 	WaitForHTTPAvailable(t, ingressIP, true)
 
+	for _, ing := range lbSvc.Status.LoadBalancer.Ingress {
+		WaitForHTTPOnServer(t, testCluster.setup.ExtServer, testCluster.setup.privKey, ing.IP, true)
+	}
+
 	lbTest.TearDown()
 }
 
@@ -171,11 +185,11 @@ func TestCloudControllerManagerNetworksPodIPsAreAccessible(t *testing.T) {
 		t.Skipf("Private Networks test is disabled")
 	}
 
-	nwTest := nwTestHelper{t: t, K8sClient: testCluster.k8sClient, server: testCluster.setup.server, privateKey: testCluster.setup.privKey, podName: "network-routes-accessible"}
+	nwTest := nwTestHelper{t: t, K8sClient: testCluster.k8sClient, privateKey: testCluster.setup.privKey, podName: "network-routes-accessible"}
 
 	pod := nwTest.DeployTestPod()
 
-	nwTest.WaitForHTTPOnServer(pod.Status.PodIP)
+	WaitForHTTPOnServer(t, testCluster.setup.ExtServer, testCluster.setup.privKey, pod.Status.PodIP, false)
 
 	nwTest.TearDown()
 }
diff --git a/e2etests/setup.go b/e2etests/setup.go
index d7daa7bc..154c1f3f 100644
--- a/e2etests/setup.go
+++ b/e2etests/setup.go
@@ -28,8 +28,9 @@ type hcloudK8sSetup struct {
 	K8sVersion     string
 	TestIdentifier string
 	KeepOnFailure  bool
+	ClusterNode    *hcloud.Server
+	ExtServer      *hcloud.Server
 	privKey        string
-	server         *hcloud.Server
 	sshKey         *hcloud.SSHKey
 	network        *hcloud.Network
 }
@@ -56,47 +57,22 @@ func (s *hcloudK8sSetup) PrepareTestEnv(ctx context.Context, additionalSSHKeys [
 	if err != nil {
 		return fmt.Errorf("%s getNetwork: %s", op, err)
 	}
-	userData, err := s.getCloudInitConfig()
-	if err != nil {
-		return fmt.Errorf("%s getCloudInitConfig: %s", op, err)
-	}
-	sshKeys := []*hcloud.SSHKey{s.sshKey}
-	for _, additionalSSHKey := range additionalSSHKeys {
-		sshKeys = append(sshKeys, additionalSSHKey)
-	}
 
-	res, _, err := s.Hcloud.Server.Create(ctx, hcloud.ServerCreateOpts{
-		Name:       fmt.Sprintf("srv-%s", s.TestIdentifier),
-		ServerType: &hcloud.ServerType{Name: "cpx21"},
-		Image:      &hcloud.Image{Name: "ubuntu-20.04"},
-		SSHKeys:    sshKeys,
-		UserData:   userData,
-		Labels:     map[string]string{"K8sVersion": s.K8sVersion, "test": s.TestIdentifier},
-		Networks:   []*hcloud.Network{s.network},
-	})
+	srv, err := s.createServer(ctx, "cluster-node", "cpx21", additionalSSHKeys)
 	if err != nil {
-		return fmt.Errorf("%s Hcloud.Server.Create: %s", op, err)
-	}
-
-	_, errCh := s.Hcloud.Action.WatchProgress(ctx, res.Action)
-	if err := <-errCh; err != nil {
-		return fmt.Errorf("%s WatchProgress Action %s: %s", op, res.Action.Command, err)
+		return fmt.Errorf("%s: create cluster node: %v", op, err)
 	}
+	s.ClusterNode = srv
 
-	for _, nextAction := range res.NextActions {
-		_, errCh := s.Hcloud.Action.WatchProgress(ctx, nextAction)
-		if err := <-errCh; err != nil {
-			return fmt.Errorf("%s WatchProgress NextAction %s: %s", op, nextAction.Command, err)
-		}
-	}
-	s.server, _, err = s.Hcloud.Server.GetByID(ctx, res.Server.ID)
+	srv, err = s.createServer(ctx, "ext-server", "cx11", additionalSSHKeys)
 	if err != nil {
-		return fmt.Errorf("%s Hcloud.Server.GetByID: %s", op, err)
+		return fmt.Errorf("%s: create ext server: %v", op, err)
 	}
+	s.ExtServer = srv
 
 	fmt.Printf("%s Waiting for server to be sshable:", op)
 	for {
-		conn, err := net.Dial("tcp", fmt.Sprintf("%s:22", s.server.PublicNet.IPv4.IP.String()))
+		conn, err := net.Dial("tcp", fmt.Sprintf("%s:22", s.ClusterNode.PublicNet.IPv4.IP.String()))
 		if err != nil {
 			fmt.Print(".")
 			time.Sleep(1 * time.Second)
@@ -117,7 +93,7 @@ func (s *hcloudK8sSetup) PrepareTestEnv(ctx context.Context, additionalSSHKeys [
 	}
 
 	fmt.Printf("%s Load Image:\n", op)
-	err = RunCommandOnServer(s.privKey, s.server, fmt.Sprintf("docker load --input ci-hcloud-ccm.tar"))
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, fmt.Sprintf("docker load --input ci-hcloud-ccm.tar"))
 	if err != nil {
 		return fmt.Errorf("%s:  Load image %s", op, err)
 	}
@@ -125,6 +101,49 @@ func (s *hcloudK8sSetup) PrepareTestEnv(ctx context.Context, additionalSSHKeys [
 	return nil
 }
 
+func (s *hcloudK8sSetup) createServer(ctx context.Context, name, typ string, additionalSSHKeys []*hcloud.SSHKey) (*hcloud.Server, error) {
+	const op = "e2etest/createServer"
+
+	userData, err := s.getCloudInitConfig()
+	if err != nil {
+		return nil, fmt.Errorf("%s getCloudInitConfig: %s", op, err)
+	}
+	sshKeys := []*hcloud.SSHKey{s.sshKey}
+	for _, additionalSSHKey := range additionalSSHKeys {
+		sshKeys = append(sshKeys, additionalSSHKey)
+	}
+
+	res, _, err := s.Hcloud.Server.Create(ctx, hcloud.ServerCreateOpts{
+		Name:       fmt.Sprintf("srv-%s-%s", name, s.TestIdentifier),
+		ServerType: &hcloud.ServerType{Name: typ},
+		Image:      &hcloud.Image{Name: "ubuntu-20.04"},
+		SSHKeys:    sshKeys,
+		UserData:   userData,
+		Labels:     map[string]string{"K8sVersion": s.K8sVersion, "test": s.TestIdentifier},
+		Networks:   []*hcloud.Network{s.network},
+	})
+	if err != nil {
+		return nil, fmt.Errorf("%s Hcloud.Server.Create: %s", op, err)
+	}
+
+	_, errCh := s.Hcloud.Action.WatchProgress(ctx, res.Action)
+	if err := <-errCh; err != nil {
+		return nil, fmt.Errorf("%s WatchProgress Action %s: %s", op, res.Action.Command, err)
+	}
+
+	for _, nextAction := range res.NextActions {
+		_, errCh := s.Hcloud.Action.WatchProgress(ctx, nextAction)
+		if err := <-errCh; err != nil {
+			return nil, fmt.Errorf("%s WatchProgress NextAction %s: %s", op, nextAction.Command, err)
+		}
+	}
+	srv, _, err := s.Hcloud.Server.GetByID(ctx, res.Server.ID)
+	if err != nil {
+		return nil, fmt.Errorf("%s Hcloud.Server.GetByID: %s", op, err)
+	}
+	return srv, nil
+}
+
 // PrepareK8s patches an existing kubernetes cluster with a CNI and the correct
 // Cloud Controller Manager version from this test run
 func (s *hcloudK8sSetup) PrepareK8s(withNetworks bool) (string, error) {
@@ -148,13 +167,13 @@ func (s *hcloudK8sSetup) PrepareK8s(withNetworks bool) (string, error) {
 	}
 
 	fmt.Printf("%s: Apply ccm deployment\n", op)
-	err = RunCommandOnServer(s.privKey, s.server, "KUBECONFIG=/root/.kube/config kubectl apply -f ccm.yml")
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, "KUBECONFIG=/root/.kube/config kubectl apply -f ccm.yml")
 	if err != nil {
 		return "", fmt.Errorf("%s Deploy ccm: %s", op, err)
 	}
 	fmt.Printf("%s: Download kubeconfig\n", op)
 
-	err = scp("ssh_key", fmt.Sprintf("root@%s:/root/.kube/config", s.server.PublicNet.IPv4.IP.String()), "kubeconfig")
+	err = scp("ssh_key", fmt.Sprintf("root@%s:/root/.kube/config", s.ClusterNode.PublicNet.IPv4.IP.String()), "kubeconfig")
 	if err != nil {
 		return "", fmt.Errorf("%s download kubeconfig: %s", op, err)
 	}
@@ -203,7 +222,7 @@ func (s *hcloudK8sSetup) prepareCCMDeploymentFile(networks bool) error {
 	deploymentFile = []byte(strings.ReplaceAll(string(deploymentFile), "hetznercloud/hcloud-cloud-controller-manager:latest", fmt.Sprintf("hcloud-ccm:ci_%s", s.TestIdentifier)))
 	deploymentFile = []byte(strings.ReplaceAll(string(deploymentFile), " imagePullPolicy: Always", " imagePullPolicy: IfNotPresent"))
 
-	err = RunCommandOnServer(s.privKey, s.server, fmt.Sprintf("echo '%s' >> ccm.yml", deploymentFile))
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, fmt.Sprintf("echo '%s' >> ccm.yml", deploymentFile))
 	if err != nil {
 		return fmt.Errorf("%s: Prepare deployment file and transfer it: %s", op, err)
 	}
@@ -215,12 +234,12 @@ func (s *hcloudK8sSetup) prepareCCMDeploymentFile(networks bool) error {
 func (s *hcloudK8sSetup) deployFlannel() error {
 	const op = "hcloudK8sSetup/deployFlannel"
 	fmt.Printf("%s: apply flannel deployment\n", op)
-	err := RunCommandOnServer(s.privKey, s.server, "KUBECONFIG=/root/.kube/config kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml")
+	err := RunCommandOnServer(s.privKey, s.ClusterNode, "KUBECONFIG=/root/.kube/config kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml")
 	if err != nil {
 		return fmt.Errorf("%s: apply flannel deployment: %s", op, err)
 	}
 	fmt.Printf("%s: patch flannel deployment\n", op)
-	err = RunCommandOnServer(s.privKey, s.server, "KUBECONFIG=/root/.kube/config kubectl -n kube-system patch ds kube-flannel-ds --type json -p '[{\"op\":\"add\",\"path\":\"/spec/template/spec/tolerations/-\",\"value\":{\"key\":\"node.cloudprovider.kubernetes.io/uninitialized\",\"value\":\"true\",\"effect\":\"NoSchedule\"}}]'")
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, "KUBECONFIG=/root/.kube/config kubectl -n kube-system patch ds kube-flannel-ds --type json -p '[{\"op\":\"add\",\"path\":\"/spec/template/spec/tolerations/-\",\"value\":{\"key\":\"node.cloudprovider.kubernetes.io/uninitialized\",\"value\":\"true\",\"effect\":\"NoSchedule\"}}]'")
 	if err != nil {
 		return fmt.Errorf("%s: patch flannel deployment: %s", op, err)
 	}
@@ -237,13 +256,13 @@ func (s *hcloudK8sSetup) deployCilium() error {
 	if err != nil {
 		return fmt.Errorf("%s: read cilium deployment file %s: %v", op, "templates/cilium.yml", err)
 	}
-	err = RunCommandOnServer(s.privKey, s.server, fmt.Sprintf("echo '%s' >> cilium.yml", deploymentFile))
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, fmt.Sprintf("echo '%s' >> cilium.yml", deploymentFile))
 	if err != nil {
 		return fmt.Errorf("%s: Transfer cilium deployment: %s", op, err)
 	}
 
 	fmt.Printf("%s: apply cilium deployment\n", op)
-	err = RunCommandOnServer(s.privKey, s.server, "KUBECONFIG=/root/.kube/config kubectl apply -f cilium.yml")
+	err = RunCommandOnServer(s.privKey, s.ClusterNode, "KUBECONFIG=/root/.kube/config kubectl apply -f cilium.yml")
 	if err != nil {
 		return fmt.Errorf("%s: apply cilium deployment: %s", op, err)
 	}
@@ -255,7 +274,7 @@ func (s *hcloudK8sSetup) deployCilium() error {
 func (s *hcloudK8sSetup) transferCCMDockerImage() error {
 	const op = "hcloudK8sSetup/transferCCMDockerImage"
 	fmt.Printf("%s: Transfer docker image\n", op)
-	err := WithSSHSession(s.privKey, s.server.PublicNet.IPv4.IP.String(), func(session *ssh.Session) error {
+	err := WithSSHSession(s.privKey, s.ClusterNode.PublicNet.IPv4.IP.String(), func(session *ssh.Session) error {
 		file, err := os.Open("ci-hcloud-ccm.tar")
 		if err != nil {
 			return fmt.Errorf("%s read ci-hcloud-ccm.tar: %s", op, err)
@@ -293,7 +312,7 @@ func (s *hcloudK8sSetup) transferCCMDockerImage() error {
 func (s *hcloudK8sSetup) waitForCloudInit() error {
 	const op = "hcloudK8sSetup/PrepareTestEnv"
 	fmt.Printf("%s: Wait for cloud-init\n", op)
-	err := RunCommandOnServer(s.privKey, s.server, fmt.Sprintf("cloud-init status --wait > /dev/null"))
+	err := RunCommandOnServer(s.privKey, s.ClusterNode, fmt.Sprintf("cloud-init status --wait > /dev/null"))
 	if err != nil {
 		return fmt.Errorf("%s: Wait for cloud-init: %s", op, err)
 	}
@@ -314,11 +333,18 @@ func (s *hcloudK8sSetup) TearDown(testFailed bool) error {
 
 	ctx := context.Background()
 
-	_, err := s.Hcloud.Server.Delete(ctx, s.server)
+	_, err := s.Hcloud.Server.Delete(ctx, s.ClusterNode)
 	if err != nil {
 		return fmt.Errorf("%s Hcloud.Server.Delete: %s", op, err)
 	}
-	s.server = nil
+	s.ClusterNode = nil
+
+	_, err = s.Hcloud.Server.Delete(ctx, s.ExtServer)
+	if err != nil {
+		return fmt.Errorf("%s Hcloud.Server.Delete: %s", op, err)
+	}
+	s.ExtServer = nil
+
 	_, err = s.Hcloud.SSHKey.Delete(ctx, s.sshKey)
 	if err != nil {
 		return fmt.Errorf("%s Hcloud.SSHKey.Delete: %s", err, err)
diff --git a/e2etests/testing.go b/e2etests/testing.go
index c359ab10..070616f0 100644
--- a/e2etests/testing.go
+++ b/e2etests/testing.go
@@ -5,6 +5,7 @@ import (
 	"crypto/tls"
 	"fmt"
 	"math/rand"
+	"net"
 	"net/http"
 	"os"
 	"os/exec"
@@ -32,11 +33,12 @@ func init() {
 }
 
 type TestCluster struct {
-	useNetworks  bool
-	setup        *hcloudK8sSetup
-	k8sClient    *kubernetes.Clientset
-	started      bool
-	certificates []*hcloud.Certificate
+	KeepOnFailure bool
+	useNetworks   bool
+	setup         *hcloudK8sSetup
+	k8sClient     *kubernetes.Clientset
+	started       bool
+	certificates  []*hcloud.Certificate
 
 	mu sync.Mutex
 }
@@ -78,7 +80,7 @@ func (tc *TestCluster) initialize() error {
 	if len(token) != 64 {
 		return fmt.Errorf("%s: No valid HCLOUD_TOKEN found", op)
 	}
-	keepOnFailure := os.Getenv("KEEP_SERVER_ON_FAILURE") == "yes"
+	tc.KeepOnFailure = os.Getenv("KEEP_SERVER_ON_FAILURE") == "yes"
 
 	var additionalSSHKeys []*hcloud.SSHKey
 
@@ -122,7 +124,7 @@ func (tc *TestCluster) initialize() error {
 		K8sVersion:     k8sVersion,
 		TestIdentifier: testIdentifier,
 		HcloudToken:    token,
-		KeepOnFailure:  keepOnFailure,
+		KeepOnFailure:  tc.KeepOnFailure,
 	}
 	fmt.Println("Setting up test env")
 
@@ -284,10 +286,11 @@ func (tc *TestCluster) CreateTLSCertificate(t *testing.T, baseName string) *hclo
 }
 
 type lbTestHelper struct {
-	podName   string
-	port      int
-	K8sClient *kubernetes.Clientset
-	t         *testing.T
+	podName       string
+	port          int
+	K8sClient     *kubernetes.Clientset
+	KeepOnFailure bool
+	t             *testing.T
 }
 
 // DeployTestPod deploys a basic nginx pod within the k8s cluster
@@ -401,6 +404,11 @@ func (l *lbTestHelper) CreateService(lbSvc *corev1.Service) (*corev1.Service, er
 // TearDown deletes the created pod and service
 func (l *lbTestHelper) TearDown() {
 	const op = "lbTestHelper/TearDown"
+
+	if l.KeepOnFailure && l.t.Failed() {
+		return
+	}
+
 	svcName := fmt.Sprintf("svc-%s", l.podName)
 	err := l.K8sClient.CoreV1().Services(corev1.NamespaceDefault).Delete(context.Background(), svcName, metav1.DeleteOptions{})
 	if err != nil && !k8serrors.IsNotFound(err) {
@@ -445,7 +453,6 @@ type nwTestHelper struct {
 	podName    string
 	K8sClient  *kubernetes.Clientset
 	privateKey string
-	server     *hcloud.Server
 	t          *testing.T
 }
 
@@ -504,24 +511,6 @@ func (n *nwTestHelper) DeployTestPod() *corev1.Pod {
 	return pod
 }
 
-// WaitForHTTPOnServer tries to connect to the given IP using curl.
-//
-// It tries it for 2 minutes, if after two minutes the connection wasn't
-// successful or it was not a HTTP 200 response it will fail
-func (n *nwTestHelper) WaitForHTTPOnServer(podIp string) {
-	const op = "nwTestHelper/WaitForHTTPAvailable"
-	err := wait.Poll(1*time.Second, 2*time.Minute, func() (bool, error) {
-		err := RunCommandOnServer(n.privateKey, n.server, fmt.Sprintf("curl http://%s", podIp))
-		if err != nil {
-			return false, nil
-		}
-		return true, nil
-	})
-	if err != nil {
-		n.t.Errorf("%s: not available via curl: %s", op, err)
-	}
-}
-
 // TearDown deletes the created pod
 func (n *nwTestHelper) TearDown() {
 	const op = "nwTestHelper/TearDown"
@@ -580,3 +569,31 @@ func WaitForHTTPAvailable(t *testing.T, ingressIP string, useHTTPS bool) {
 		t.Errorf("%s: not available via client.Get: %s", op, err)
 	}
 }
+
+// WaitForHTTPOnServer tries to connect to the given IP using curl.
+//
+// It tries it for 2 minutes, if after two minutes the connection wasn't
+// successful or it was not a HTTP 200 response it will fail
+func WaitForHTTPOnServer(t *testing.T, srv *hcloud.Server, privateKey, tgtIP string, useHTTPS bool) {
+	const op = "e2etests/WaitForHTTPOnServer"
+
+	proto := "http"
+	if useHTTPS {
+		proto = "https"
+	}
+	cmd := fmt.Sprintf("curl -k %s://%s", proto, tgtIP)
+	if net.ParseIP(tgtIP).To4() == nil {
+		// Assume its a IPv6 address
+		cmd = fmt.Sprintf("curl -6 -kg %s://[%s]", proto, tgtIP)
+	}
+
+	err := wait.Poll(1*time.Second, 2*time.Minute, func() (bool, error) {
+		if err := RunCommandOnServer(privateKey, srv, cmd); err != nil {
+			return false, nil
+		}
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("%s: not available via %q: %s", op, cmd, err)
+	}
+}
diff --git a/hcloud/load_balancers_test.go b/hcloud/load_balancers_test.go
index c71402f2..4cff3ac7 100644
--- a/hcloud/load_balancers_test.go
+++ b/hcloud/load_balancers_test.go
@@ -22,7 +22,7 @@ func TestLoadBalancers_GetLoadBalancer(t *testing.T) {
 				Name: "no-host-name",
 				PublicNet: hcloud.LoadBalancerPublicNet{
 					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")},
 				},
 			},
 			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
@@ -35,10 +35,11 @@ func TestLoadBalancers_GetLoadBalancer(t *testing.T) {
 				assert.NoError(t, err)
 				assert.True(t, exists)
 
-				if !assert.Len(t, status.Ingress, 1) {
+				if !assert.Len(t, status.Ingress, 2) {
 					return
 				}
 				assert.Equal(t, tt.LB.PublicNet.IPv4.IP.String(), status.Ingress[0].IP)
+				assert.Equal(t, tt.LB.PublicNet.IPv6.IP.String(), status.Ingress[1].IP)
 			},
 		},
 		{
@@ -124,7 +125,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")},
 				},
 			},
 			Mock: func(t *testing.T, tt *LoadBalancerTestCase) {
@@ -151,7 +152,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				expected := &v1.LoadBalancerStatus{
 					Ingress: []v1.LoadBalancerIngress{
 						{IP: tt.LB.PublicNet.IPv4.IP.String()},
-						// {IP: tt.LB.PublicNet.IPv6.IP.String()},
+						{IP: tt.LB.PublicNet.IPv6.IP.String()},
 					},
 				}
 				lbStat, err := tt.LoadBalancers.EnsureLoadBalancer(tt.Ctx, tt.ClusterName, tt.Service, tt.Nodes)
@@ -174,7 +175,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{
 					{
@@ -210,7 +211,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				expected := &v1.LoadBalancerStatus{
 					Ingress: []v1.LoadBalancerIngress{
 						{IP: tt.LB.PublicNet.IPv4.IP.String()},
-						// {IP: tt.LB.PublicNet.IPv6.IP.String()},
+						{IP: tt.LB.PublicNet.IPv6.IP.String()},
 						{IP: tt.LB.PrivateNet[0].IP.String()},
 					},
 				}
@@ -271,7 +272,7 @@ func TestLoadBalancers_EnsureLoadBalancer_CreateLoadBalancer(t *testing.T) {
 				expected := &v1.LoadBalancerStatus{
 					Ingress: []v1.LoadBalancerIngress{
 						{IP: tt.LB.PublicNet.IPv4.IP.String()},
-						// {IP: tt.LB.PublicNet.IPv6.IP.String()},
+						{IP: tt.LB.PublicNet.IPv6.IP.String()},
 					},
 				}
 				lbStat, err := tt.LoadBalancers.EnsureLoadBalancer(tt.Ctx, tt.ClusterName, tt.Service, tt.Nodes)