Repository: syself/cluster-api-provider-hetzner
Pull request #891 (2023-09-02 14:53:29)

:sparkles: Reuse existing load balancer
**What this PR does / why we need it**:
Re-using existing load balancer if name of load balancer is set in spec

**TODOs**:
- [x] squash commits
- [ ] include documentation
- [x] add unit tests




Patch:
diff --git a/api/v1beta1/conditions_const.go b/api/v1beta1/conditions_const.go
index 5c72a397..f0e7cb55 100644
--- a/api/v1beta1/conditions_const.go
+++ b/api/v1beta1/conditions_const.go
@@ -23,6 +23,10 @@ const (
 	LoadBalancerReadyCondition clusterv1.ConditionType = "LoadBalancerReady"
 	// LoadBalancerFailedReason used when an error occurs during load balancer reconciliation.
 	LoadBalancerFailedReason = "LoadBalancerFailed"
+	// LoadBalancerNotFoundReason used when a load balancer could not be found.
+	LoadBalancerNotFoundReason = "LoadBalancerNotFound"
+	// LoadBalancerFailedToOwnReason used when no owned label could be set on a load balancer.
+	LoadBalancerFailedToOwnReason = "LoadBalancerFailedToOwn"
 )
 
 const (
diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go
index a0dc92e3..7cdc0c64 100644
--- a/controllers/hetznercluster_controller.go
+++ b/controllers/hetznercluster_controller.go
@@ -119,8 +119,6 @@ func (r *HetznerClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 		return reconcile.Result{}, nil
 	}
 
-	log = log.WithValues("cluster", cluster.Name)
-
 	// Create the scope.
 	secretManager := secretutil.NewSecretManager(log, r.Client, r.APIReader)
 	hcloudToken, hetznerSecret, err := getAndValidateHCloudToken(ctx, req.Namespace, hetznerCluster, secretManager)
@@ -183,11 +181,17 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS
 		return reconcile.Result{}, fmt.Errorf("failed to reconcile network for HetznerCluster %s/%s: %w", hetznerCluster.Namespace, hetznerCluster.Name, err)
 	}
 
+	emptyResult := reconcile.Result{}
+
 	// reconcile the load balancers
-	if err := loadbalancer.NewService(clusterScope).Reconcile(ctx); err != nil {
-		conditions.MarkFalse(hetznerCluster, infrav1.LoadBalancerReadyCondition, infrav1.LoadBalancerFailedReason, clusterv1.ConditionSeverityError, err.Error())
+	res, err := loadbalancer.NewService(clusterScope).Reconcile(ctx)
+	if res != emptyResult {
+		return res, nil
+	}
+	if err != nil {
 		return reconcile.Result{}, fmt.Errorf("failed to reconcile load balancers for HetznerCluster %s/%s: %w", hetznerCluster.Namespace, hetznerCluster.Name, err)
 	}
+
 	conditions.MarkTrue(hetznerCluster, infrav1.LoadBalancerReadyCondition)
 
 	// reconcile the placement groups
@@ -226,8 +230,6 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS
 	if err != nil {
 		return reconcile.Result{}, fmt.Errorf("failed to reconcile target cluster manager: %w", err)
 	}
-
-	emptyResult := reconcile.Result{}
 	if result != emptyResult {
 		return result, nil
 	}
diff --git a/pkg/services/hcloud/client/fake/hcloud_client.go b/pkg/services/hcloud/client/fake/hcloud_client.go
index 47976fd7..af0f18f8 100644
--- a/pkg/services/hcloud/client/fake/hcloud_client.go
+++ b/pkg/services/hcloud/client/fake/hcloud_client.go
@@ -185,7 +185,13 @@ func (c *cacheHCloudClient) ListLoadBalancers(_ context.Context, opts hcloud.Loa
 		return nil, fmt.Errorf("failed to convert label selector to labels: %w", err)
 	}
 
-	for _, lb := range c.loadBalancerCache.idMap {
+	for key, lb := range c.loadBalancerCache.idMap {
+		// if name is set and is not correct, continue
+		if opts.Name != "" && lb.Name != opts.Name {
+			continue
+		}
+
+		// check for labels
 		allLabelsFound := true
 		for key, label := range labels {
 			if val, found := lb.Labels[key]; !found || val != label {
@@ -193,8 +199,9 @@ func (c *cacheHCloudClient) ListLoadBalancers(_ context.Context, opts hcloud.Loa
 				break
 			}
 		}
+
 		if allLabelsFound {
-			lbs = append(lbs, lb)
+			lbs = append(lbs, c.loadBalancerCache.idMap[key])
 		}
 	}
 	return lbs, nil
diff --git a/pkg/services/hcloud/loadbalancer/loadbalancer.go b/pkg/services/hcloud/loadbalancer/loadbalancer.go
index af3ef7d4..749f24ba 100644
--- a/pkg/services/hcloud/loadbalancer/loadbalancer.go
+++ b/pkg/services/hcloud/loadbalancer/loadbalancer.go
@@ -21,12 +21,15 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"strings"
+	"time"
 
 	"github.com/go-logr/logr"
 	"github.com/hetznercloud/hcloud-go/hcloud"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util/conditions"
 	"sigs.k8s.io/cluster-api/util/record"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
 	"github.com/syself/cluster-api-provider-hetzner/pkg/scope"
@@ -44,10 +47,13 @@ func NewService(scope *scope.ClusterScope) *Service {
 	return &Service{scope: scope}
 }
 
+// ErrNoLoadBalancerAvailable indicates that no available load balancer could be fond.
+var ErrNoLoadBalancerAvailable = fmt.Errorf("no available load balancer")
+
 // Reconcile implements the life cycle of HCloud load balancers.
-func (s *Service) Reconcile(ctx context.Context) error {
+func (s *Service) Reconcile(ctx context.Context) (reconcile.Result, error) {
 	if !s.scope.HetznerCluster.Spec.ControlPlaneLoadBalancer.Enabled {
-		return nil
+		return reconcile.Result{}, nil
 	}
 
 	log := s.scope.Logger.WithValues("reconciler", "load balancer")
@@ -55,14 +61,24 @@ func (s *Service) Reconcile(ctx context.Context) error {
 	// find load balancer
 	lb, err := s.findLoadBalancer(ctx)
 	if err != nil {
-		return fmt.Errorf("failed to find load balancer: %w", err)
+		return reconcile.Result{}, fmt.Errorf("failed to find load balancer: %w", err)
 	}
 
 	if lb == nil {
-		log.V(1).Info("Create a new loadbalancer", "algorithm type", s.scope.HetznerCluster.Spec.ControlPlaneLoadBalancer.Algorithm)
-		lb, err = s.createLoadBalancer(ctx)
+		// fixed name is set - we expect a load balancer with this name to exist
+
+		if s.scope.HetznerCluster.Spec.ControlPlaneLoadBalancer.Name != nil {
+			lb, err = s.ownExistingLoadBalancer(ctx)
+
+			// if load balancer is not found even though we expect it to exist, wait and reconcile until user creates it
+			if errors.Is(err, ErrNoLoadBalancerAvailable) {
+				return reconcile.Result{RequeueAfter: 1 * time.Minute}, nil
+			}
+		} else {
+			lb, err = s.createLoadBalancer(ctx)
+		}
 		if err != nil {
-			return fmt.Errorf("failed to create load balancer: %w", err)
+			return reconcile.Result{}, fmt.Errorf("failed to own/create load balancer: %w", err)
 		}
 	}
 
@@ -70,18 +86,18 @@ func (s *Service) Reconcile(ctx context.Context) error {
 
 	// check whether load balancer name, algorithm or type has been changed
 	if err := s.reconcileLBProperties(ctx, lb); err != nil {
-		return fmt.Errorf("failed to reconcile load balancer properties: %w", err)
+		return reconcile.Result{}, fmt.Errorf("failed to reconcile load balancer properties: %w", err)
 	}
 
 	if err := s.reconcileNetworkAttachement(ctx, lb); err != nil {
-		return fmt.Errorf("failed to reconcile network attachement: %w", err)
+		return reconcile.Result{}, fmt.Errorf("failed to reconcile network attachement: %w", err)
 	}
 
 	if err := s.reconcileServices(ctx, lb); err != nil {
-		return fmt.Errorf("failed to reconcile services: %w", err)
+		return reconcile.Result{}, fmt.Errorf("failed to reconcile services: %w", err)
 	}
 
-	return nil
+	return reconcile.Result{}, nil
 }
 
 func (s *Service) reconcileNetworkAttachement(ctx context.Context, lb *hcloud.LoadBalancer) error {
@@ -251,6 +267,8 @@ func (s *Service) createLoadBalancer(ctx context.Context) (*hcloud.LoadBalancer,
 		hcloudutil.HandleRateLimitExceeded(s.scope.HetznerCluster, err, "CreateLoadBalancer")
 		err = fmt.Errorf("failed to create load balancer: %w", err)
 		record.Warnf(s.scope.HetznerCluster, "FailedCreateLoadBalancer", err.Error())
+		conditions.MarkFalse(s.scope.HetznerCluster, infrav1.LoadBalancerReadyCondition, infrav1.LoadBalancerFailedReason, clusterv1.ConditionSeverityError, err.Error())
+
 		return nil, err
 	}
 
@@ -263,7 +281,7 @@ func createOptsFromSpec(hc *infrav1.HetznerCluster) hcloud.LoadBalancerCreateOpt
 	algorithmType := hc.Spec.ControlPlaneLoadBalancer.Algorithm.HCloudAlgorithmType()
 
 	// Set name
-	name := utils.GenerateName(hc.Spec.ControlPlaneLoadBalancer.Name, fmt.Sprintf("%s-kube-apiserver-", hc.Name))
+	name := utils.GenerateName(nil, fmt.Sprintf("%s-kube-apiserver-", hc.Name))
 
 	proxyprotocol := false
 
@@ -346,6 +364,60 @@ func (s *Service) findLoadBalancer(ctx context.Context) (*hcloud.LoadBalancer, e
 	return loadBalancers[0], nil
 }
 
+func (s *Service) ownExistingLoadBalancer(ctx context.Context) (*hcloud.LoadBalancer, error) {
+	name := *s.scope.HetznerCluster.Spec.ControlPlaneLoadBalancer.Name
+	loadBalancers, err := s.scope.HCloudClient.ListLoadBalancers(ctx, hcloud.LoadBalancerListOpts{Name: name})
+	if err != nil {
+		hcloudutil.HandleRateLimitExceeded(s.scope.HetznerCluster, err, "ListLoadBalancers")
+		return nil, fmt.Errorf("failed to list load balancers: %w", err)
+	}
+
+	if len(loadBalancers) > 1 {
+		return nil, fmt.Errorf("found %v load balancers in HCloud with name %q", len(loadBalancers), name)
+	}
+
+	if len(loadBalancers) == 0 {
+		conditions.MarkFalse(
+			s.scope.HetznerCluster,
+			infrav1.LoadBalancerReadyCondition,
+			infrav1.LoadBalancerNotFoundReason,
+			clusterv1.ConditionSeverityError,
+			fmt.Sprintf("load balancer %q not found", name),
+		)
+		return nil, ErrNoLoadBalancerAvailable
+	}
+
+	lb := loadBalancers[0]
+
+	for label := range lb.Labels {
+		if strings.HasPrefix(label, infrav1.NameHetznerProviderOwned) {
+			conditions.MarkFalse(
+				s.scope.HetznerCluster,
+				infrav1.LoadBalancerReadyCondition,
+				infrav1.LoadBalancerFailedToOwnReason,
+				clusterv1.ConditionSeverityError,
+				fmt.Sprintf("load balancer %q owned with label %q", name, label),
+			)
+			return nil, ErrNoLoadBalancerAvailable
+		}
+	}
+
+	if lb.Labels == nil {
+		lb.Labels = make(map[string]string)
+	}
+	lb.Labels[s.scope.HetznerCluster.ClusterTagKey()] = string(infrav1.ResourceLifecycleOwned)
+
+	lb, err = s.scope.HCloudClient.UpdateLoadBalancer(ctx, lb, hcloud.LoadBalancerUpdateOpts{Labels: lb.Labels})
+	if err != nil {
+		hcloudutil.HandleRateLimitExceeded(s.scope.HetznerCluster, err, "UpdateLoadBalancer")
+		err = fmt.Errorf("failed to update load balancer: %w", err)
+		record.Warnf(s.scope.HetznerCluster, "FailedUpdateLoadBalancer", err.Error())
+		return nil, err
+	}
+
+	return lb, nil
+}
+
 // statusFromHCloudLB gets the information of the Hetzner load balancer and returns it in the status object.
 func statusFromHCloudLB(lb *hcloud.LoadBalancer, hasNetwork bool, log logr.Logger) *infrav1.LoadBalancerStatus {
 	var internalIP string


Test patch:
diff --git a/controllers/hetznercluster_controller_test.go b/controllers/hetznercluster_controller_test.go
index 9bdb6b58..9e58cc88 100644
--- a/controllers/hetznercluster_controller_test.go
+++ b/controllers/hetznercluster_controller_test.go
@@ -111,20 +111,32 @@ var _ = Describe("Hetzner ClusterReconciler", func() {
 	})
 
 	Context("load balancer", func() {
-		It("should create load balancer and update it accordingly", func() {
-			testNs, err := testEnv.CreateNamespace(ctx, "lb-attachement")
+		var (
+			err             error
+			namespace       string
+			testNs          *corev1.Namespace
+			hetznerSecret   *corev1.Secret
+			bootstrapSecret *corev1.Secret
+
+			instance    *infrav1.HetznerCluster
+			capiCluster *clusterv1.Cluster
+
+			lbName string
+		)
+		BeforeEach(func() {
+			testNs, err = testEnv.CreateNamespace(ctx, "lb-attachement")
 			Expect(err).NotTo(HaveOccurred())
-			namespace := testNs.Name
+			namespace = testNs.Name
+
+			lbName = utils.GenerateName(nil, "myloadbalancer")
+
 			// Create the secret
-			hetznerSecret := getDefaultHetznerSecret(namespace)
+			hetznerSecret = getDefaultHetznerSecret(namespace)
 			Expect(testEnv.Create(ctx, hetznerSecret)).To(Succeed())
-			defer func() {
-				Expect(testEnv.Cleanup(ctx, hetznerSecret)).To(Succeed())
-			}()
 
 			hetznerClusterName := utils.GenerateName(nil, "hetzner-test1")
 			// Create capi cluster
-			capiCluster := &clusterv1.Cluster{
+			capiCluster = &clusterv1.Cluster{
 				ObjectMeta: metav1.ObjectMeta{
 					GenerateName: "test1-",
 					Namespace:    namespace,
@@ -139,12 +151,9 @@ var _ = Describe("Hetzner ClusterReconciler", func() {
 				},
 			}
 			Expect(testEnv.Create(ctx, capiCluster)).To(Succeed())
-			defer func() {
-				Expect(testEnv.Cleanup(ctx, capiCluster)).To(Succeed())
-			}()
 
 			// Create the HetznerCluster object
-			instance := &infrav1.HetznerCluster{
+			instance = &infrav1.HetznerCluster{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      hetznerClusterName,
 					Namespace: namespace,
@@ -159,10 +168,14 @@ var _ = Describe("Hetzner ClusterReconciler", func() {
 				},
 				Spec: getDefaultHetznerClusterSpec(),
 			}
+		})
+
+		AfterEach(func() {
+			Expect(testEnv.Cleanup(ctx, testNs, capiCluster, hetznerSecret, instance)).To(Succeed())
+		})
+
+		It("should create load balancer and update it accordingly", func() {
 			Expect(testEnv.Create(ctx, instance)).To(Succeed())
-			defer func() {
-				Expect(testEnv.Cleanup(ctx, instance)).To(Succeed())
-			}()
 
 			key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
 
@@ -314,94 +327,144 @@ var _ = Describe("Hetzner ClusterReconciler", func() {
 			}, timeout).Should(Equal(len(instance.Spec.ControlPlaneLoadBalancer.ExtraServices)))
 		})
 
-		Context("should not create load balancer if disabled", func() {
-			var (
-				namespace       string
-				testNs          *corev1.Namespace
-				hetznerSecret   *corev1.Secret
-				bootstrapSecret *corev1.Secret
-
-				instance    *infrav1.HetznerCluster
-				capiCluster *clusterv1.Cluster
-			)
+		It("should not create load balancer if disabled and the cluster should get ready", func() {
+			// Create the bootstrap secret
+			bootstrapSecret = getDefaultBootstrapSecret(namespace)
+			Expect(testEnv.Create(ctx, bootstrapSecret)).To(Succeed())
 
-			BeforeEach(func() {
-				var err error
-				testNs, err = testEnv.CreateNamespace(ctx, "lb-disabled")
-				Expect(err).NotTo(HaveOccurred())
-				namespace = testNs.Name
-
-				// Create the hetzner secret
-				hetznerSecret = getDefaultHetznerSecret(namespace)
-				Expect(testEnv.Create(ctx, hetznerSecret)).To(Succeed())
-				// Create the bootstrap secret
-				bootstrapSecret = getDefaultBootstrapSecret(namespace)
-				Expect(testEnv.Create(ctx, bootstrapSecret)).To(Succeed())
+			defer func() {
+				Expect(testEnv.Delete(ctx, bootstrapSecret)).To(Succeed())
+			}()
 
-				hetznerClusterName := utils.GenerateName(nil, "test1-")
+			instance.Spec.ControlPlaneLoadBalancer.Enabled = false
+			instance.Spec.ControlPlaneEndpoint = &clusterv1.APIEndpoint{
+				Host: "my.test.host",
+				Port: 6443,
+			}
+			Expect(testEnv.Create(ctx, instance)).To(Succeed())
 
-				capiCluster = &clusterv1.Cluster{
-					ObjectMeta: metav1.ObjectMeta{
-						GenerateName: "capi-test1-",
-						Namespace:    namespace,
-						Finalizers:   []string{clusterv1.ClusterFinalizer},
-					},
-					Spec: clusterv1.ClusterSpec{
-						InfrastructureRef: &corev1.ObjectReference{
-							APIVersion: infrav1.GroupVersion.String(),
-							Kind:       "HetznerCluster",
-							Name:       hetznerClusterName,
-							Namespace:  namespace,
-						},
-					},
+			key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
+			Eventually(func() bool {
+				if err := testEnv.Get(ctx, key, instance); err != nil {
+					return false
 				}
-				Expect(testEnv.Create(ctx, capiCluster)).To(Succeed())
 
-				instance = &infrav1.HetznerCluster{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      hetznerClusterName,
-						Namespace: namespace,
-						OwnerReferences: []metav1.OwnerReference{
-							{
-								APIVersion: "cluster.x-k8s.io/v1beta1",
-								Kind:       "Cluster",
-								Name:       capiCluster.Name,
-								UID:        capiCluster.UID,
-							},
-						},
-					},
-					Spec: getDefaultHetznerClusterSpec(),
+				return instance.Status.ControlPlaneLoadBalancer == nil && instance.Status.Ready
+			}, timeout, time.Second).Should(BeTrue())
+		})
+
+		It("should take over an existing load balancer with correct name", func() {
+			// Create the bootstrap secret
+			bootstrapSecret = getDefaultBootstrapSecret(namespace)
+			Expect(testEnv.Create(ctx, bootstrapSecret)).To(Succeed())
+
+			defer func() {
+				Expect(testEnv.Delete(ctx, bootstrapSecret)).To(Succeed())
+			}()
+
+			hcloudClient := testEnv.HCloudClientFactory.NewClient("")
+
+			By("creating load balancer manually")
+
+			opts := hcloud.LoadBalancerCreateOpts{
+				Name:             lbName,
+				Algorithm:        &hcloud.LoadBalancerAlgorithm{Type: hcloud.LoadBalancerAlgorithmTypeLeastConnections},
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "mytype"},
+			}
+
+			_, err := hcloudClient.CreateLoadBalancer(ctx, opts)
+			Expect(err).To(BeNil())
+
+			By("creating cluster object")
+			instance.Spec.ControlPlaneLoadBalancer.Name = &lbName
+			Expect(testEnv.Create(ctx, instance)).To(Succeed())
+
+			By("checking that cluster is ready")
+			key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
+
+			Eventually(func() bool {
+				var foundInstance infrav1.HetznerCluster
+				if err := testEnv.Get(ctx, key, &foundInstance); err != nil {
+					return false
 				}
-				instance.Spec.ControlPlaneLoadBalancer.Enabled = false
-				instance.Spec.ControlPlaneEndpoint = &clusterv1.APIEndpoint{
-					Host: "my.test.host",
-					Port: 6443,
+
+				return isPresentAndTrue(key, instance, infrav1.LoadBalancerReadyCondition)
+			}, timeout, time.Second).Should(BeTrue())
+
+			By("checking that load balancer has label set")
+			loadBalancers, err := hcloudClient.ListLoadBalancers(ctx, hcloud.LoadBalancerListOpts{Name: lbName})
+			Expect(err).To(BeNil())
+			Expect(loadBalancers).To(HaveLen(1))
+
+			value, found := loadBalancers[0].Labels[instance.ClusterTagKey()]
+			Expect(found).To(BeTrue())
+			Expect(value).To(Equal(string(infrav1.ResourceLifecycleOwned)))
+		})
+
+		It("should set the appropriate condition if a named load balancer is taken by another cluster", func() {
+			// Create the bootstrap secret
+			bootstrapSecret = getDefaultBootstrapSecret(namespace)
+			Expect(testEnv.Create(ctx, bootstrapSecret)).To(Succeed())
+
+			defer func() {
+				Expect(testEnv.Delete(ctx, bootstrapSecret)).To(Succeed())
+			}()
+
+			hcloudClient := testEnv.HCloudClientFactory.NewClient("")
+
+			By("creating load balancer manually")
+			labelsOwnedByOtherCluster := map[string]string{instance.ClusterTagKey() + "s": string(infrav1.ResourceLifecycleOwned)}
+			opts := hcloud.LoadBalancerCreateOpts{
+				Name:             lbName,
+				Algorithm:        &hcloud.LoadBalancerAlgorithm{Type: hcloud.LoadBalancerAlgorithmTypeLeastConnections},
+				LoadBalancerType: &hcloud.LoadBalancerType{Name: "mytype"},
+				Labels:           labelsOwnedByOtherCluster,
+			}
+
+			_, err := hcloudClient.CreateLoadBalancer(ctx, opts)
+			Expect(err).To(BeNil())
+
+			By("creating cluster object")
+			instance.Spec.ControlPlaneLoadBalancer.Name = &lbName
+			Expect(testEnv.Create(ctx, instance)).To(Succeed())
+
+			By("checking that cluster is ready")
+			key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
+
+			Eventually(func() bool {
+				var foundInstance infrav1.HetznerCluster
+				if err := testEnv.Get(ctx, key, &foundInstance); err != nil {
+					return false
 				}
-				Expect(testEnv.Create(ctx, instance)).To(Succeed())
-			})
 
-			AfterEach(func() {
+				return isPresentAndFalseWithReason(key, instance, infrav1.LoadBalancerReadyCondition, infrav1.LoadBalancerFailedToOwnReason)
+			}, timeout, time.Second).Should(BeTrue())
+		})
+
+		It("should set the appropriate condition if a named load balancer is not found", func() {
+			// Create the bootstrap secret
+			bootstrapSecret = getDefaultBootstrapSecret(namespace)
+			Expect(testEnv.Create(ctx, bootstrapSecret)).To(Succeed())
+
+			defer func() {
 				Expect(testEnv.Delete(ctx, bootstrapSecret)).To(Succeed())
-				Expect(testEnv.Delete(ctx, hetznerSecret)).To(Succeed())
-				Expect(testEnv.Delete(ctx, capiCluster)).To(Succeed())
-				Expect(testEnv.Delete(ctx, instance)).To(Succeed())
-				Expect(testEnv.Delete(ctx, testNs)).To(Succeed())
-			})
+			}()
 
-			It("should not create load balancer and cluster should be ready", func() {
-				key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
-				Eventually(func() bool {
-					if err := testEnv.Get(ctx, key, instance); err != nil {
-						fmt.Println("Did not find instance")
-						return false
-					}
+			By("creating cluster object")
+			instance.Spec.ControlPlaneLoadBalancer.Name = &lbName
+			Expect(testEnv.Create(ctx, instance)).To(Succeed())
 
-					fmt.Println(instance.Status.ControlPlaneLoadBalancer)
-					fmt.Println(instance.Status.Ready)
+			By("checking that cluster has condition set")
+			key := client.ObjectKey{Namespace: instance.Namespace, Name: instance.Name}
 
-					return instance.Status.ControlPlaneLoadBalancer == nil && instance.Status.Ready
-				}, timeout, time.Second).Should(BeTrue())
-			})
+			Eventually(func() bool {
+				var foundInstance infrav1.HetznerCluster
+				if err := testEnv.Get(ctx, key, &foundInstance); err != nil {
+					return false
+				}
+
+				return isPresentAndFalseWithReason(key, instance, infrav1.LoadBalancerReadyCondition, infrav1.LoadBalancerNotFoundReason)
+			}, timeout, time.Second).Should(BeTrue())
 		})
 	})
 
diff --git a/pkg/services/hcloud/client/fake/hcloud_client_test.go b/pkg/services/hcloud/client/fake/hcloud_client_test.go
index f7c0676f..d8e3734d 100644
--- a/pkg/services/hcloud/client/fake/hcloud_client_test.go
+++ b/pkg/services/hcloud/client/fake/hcloud_client_test.go
@@ -100,6 +100,12 @@ var _ = Describe("Load balancer", func() {
 		Expect(lb.ID).ToNot(Equal(0))
 	})
 
+	It("lists a load balancer by name", func() {
+		resp, err := client.ListLoadBalancers(ctx, hcloud.LoadBalancerListOpts{Name: lb.Name})
+		Expect(err).To(BeNil())
+		Expect(resp).To(HaveLen(1))
+	})
+
 	It("gives an error when load balancer is created twice", func() {
 		_, err := client.CreateLoadBalancer(ctx, opts)
 		Expect(err).ToNot(Succeed())
diff --git a/pkg/services/hcloud/loadbalancer/loadbalancer_test.go b/pkg/services/hcloud/loadbalancer/loadbalancer_test.go
index 6bc57585..7dd8f2c7 100644
--- a/pkg/services/hcloud/loadbalancer/loadbalancer_test.go
+++ b/pkg/services/hcloud/loadbalancer/loadbalancer_test.go
@@ -73,7 +73,6 @@ var _ = Describe("createOptsFromSpec", func() {
 	var hetznerCluster *infrav1.HetznerCluster
 	var wantCreateOpts hcloud.LoadBalancerCreateOpts
 	BeforeEach(func() {
-		lbName := "lb-name"
 		lbType := "lb11"
 		lbRegion := "fsn1"
 		controlPlaneEndpointPort := 22
@@ -83,7 +82,7 @@ var _ = Describe("createOptsFromSpec", func() {
 		hetznerCluster = &infrav1.HetznerCluster{
 			Spec: infrav1.HetznerClusterSpec{
 				ControlPlaneLoadBalancer: infrav1.LoadBalancerSpec{
-					Name:      &lbName,
+					Name:      nil,
 					Algorithm: infrav1.LoadBalancerAlgorithmTypeLeastConnections,
 					Type:      lbType,
 					Region:    infrav1.Region(lbRegion),
@@ -102,7 +101,7 @@ var _ = Describe("createOptsFromSpec", func() {
 
 		wantCreateOpts = hcloud.LoadBalancerCreateOpts{
 			LoadBalancerType: &hcloud.LoadBalancerType{Name: lbType},
-			Name:             lbName,
+			Name:             "",
 			Algorithm:        &hcloud.LoadBalancerAlgorithm{Type: hcloud.LoadBalancerAlgorithmTypeLeastConnections},
 			Location:         &hcloud.Location{Name: lbRegion},
 			Network:          &hcloud.Network{ID: networkID},
@@ -123,11 +122,21 @@ var _ = Describe("createOptsFromSpec", func() {
 		hetznerCluster.Status.Network = nil
 		wantCreateOpts.Network = nil
 
-		Expect(createOptsFromSpec(hetznerCluster)).To(Equal(wantCreateOpts))
+		createOpts := createOptsFromSpec(hetznerCluster)
+
+		// ignore random name
+		createOpts.Name = ""
+
+		Expect(createOpts).To(Equal(wantCreateOpts))
 	})
 
 	It("creates specs for cluster with network", func() {
-		Expect(createOptsFromSpec(hetznerCluster)).To(Equal(wantCreateOpts))
+		createOpts := createOptsFromSpec(hetznerCluster)
+
+		// ignore random name
+		createOpts.Name = ""
+
+		Expect(createOpts).To(Equal(wantCreateOpts))
 	})
 
 	It("creates specs for cluster without load balancer name set", func() {