Repository: syself/cluster-api-provider-hetzner (go)

Issue:
Don't show rate limit errors on running machines 
/kind feature

**Describe the solution you'd like**
On machines that are fully provisioned and that periodically reconcile we don't need to show that we hit the rate limit. This makes people believe that something is wrong where there is not. 

PR: https://github.com/syself/cluster-api-provider-hetzner/pull/1387

Pull request description:
🌱Add handleRateLimit method
<!-- If this is your first PR, welcome! Please make sure you read the [contributing guidelines](../CONTRIBUTING.md#contributing-a-patch). -->
<!-- please add an icon to the title of this PR and delete this line and similar ones -->
<!-- the icon will be either ⚠️ (:warning:, major or breaking changes), ✨ (:sparkles:, feature additions), 🐛 (:bug:, patches and bugfixes), 📖 (:book:, documentation or proposals), or 🌱 (:seedling:, minor or other) -->

**What this PR does / why we need it**:

**Which issue(s) this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*:
Fixes #1340 

**Special notes for your reviewer**:

_Please confirm that if this PR changes any image versions, then that's the sole change this PR makes._

**TODOs**:

- [ ] squash commits
- [ ] include documentation
- [ ] add unit tests



Patch:
diff --git a/api/v1beta1/hcloudmachine_types.go b/api/v1beta1/hcloudmachine_types.go
index 19f5c489..8825f48e 100644
--- a/api/v1beta1/hcloudmachine_types.go
+++ b/api/v1beta1/hcloudmachine_types.go
@@ -24,10 +24,13 @@ import (
 )
 
 const (
-	// MachineFinalizer allows ReconcileHCloudMachine to clean up HCloud
+	// HCloudMachineFinalizer allows ReconcileHCloudMachine to clean up HCloud
 	// resources associated with HCloudMachine before removing it from the
 	// apiserver.
-	MachineFinalizer = "hcloudmachine.infrastructure.cluster.x-k8s.io"
+	HCloudMachineFinalizer = "infrastructure.cluster.x-k8s.io/hcloudmachine"
+	// DeprecatedHCloudMachineFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedHCloudMachineFinalizer = "hcloudmachine.infrastructure.cluster.x-k8s.io"
 )
 
 // HCloudMachineSpec defines the desired state of HCloudMachine.
diff --git a/api/v1beta1/hcloudremediation_types.go b/api/v1beta1/hcloudremediation_types.go
index e77fc819..07d1920b 100644
--- a/api/v1beta1/hcloudremediation_types.go
+++ b/api/v1beta1/hcloudremediation_types.go
@@ -22,10 +22,6 @@ import (
 )
 
 const (
-	// HCloudRemediationFinalizer allows HCloudRemediationReconciler to clean up resources associated with HCloudRemediation before
-	// removing it from the apiserver.
-	HCloudRemediationFinalizer = "hcloudremediation.infrastructure.cluster.x-k8s.io"
-
 	// HCloudRebootAnnotation indicates that a bare metal host object should be rebooted.
 	HCloudRebootAnnotation = "reboot.hcloud.infrastructure.cluster.x-k8s.io"
 )
diff --git a/api/v1beta1/hetznerbaremetalhost_types.go b/api/v1beta1/hetznerbaremetalhost_types.go
index 51456cbd..801ded91 100644
--- a/api/v1beta1/hetznerbaremetalhost_types.go
+++ b/api/v1beta1/hetznerbaremetalhost_types.go
@@ -31,10 +31,14 @@ import (
 )
 
 const (
-	// BareMetalHostFinalizer is the name of the finalizer added to
+	// HetznerBareMetalHostFinalizer is the name of the finalizer added to
 	// hosts to block delete operations until the physical host can be
 	// deprovisioned.
-	BareMetalHostFinalizer string = "hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
+	HetznerBareMetalHostFinalizer = "infrastructure.cluster.x-k8s.io/hetznerbaremetalhost"
+
+	// DeprecatedBareMetalHostFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedBareMetalHostFinalizer = "hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
 
 	// HostAnnotation is the key for an annotation that should go on a HetznerBareMetalMachine to
 	// reference what HetznerBareMetalHost it corresponds to.
@@ -132,17 +136,17 @@ const (
 
 const (
 	// ErrorMessageMissingRootDeviceHints specifies the error message when no root device hints are specified.
-	ErrorMessageMissingRootDeviceHints string = "no root device hints specified"
+	ErrorMessageMissingRootDeviceHints = "no root device hints specified"
 	// ErrorMessageInvalidRootDeviceHints specifies the error message when invalid root device hints are specified.
-	ErrorMessageInvalidRootDeviceHints string = "invalid root device hints specified"
+	ErrorMessageInvalidRootDeviceHints = "invalid root device hints specified"
 	// ErrorMessageMissingHetznerSecret specifies the error message when no Hetzner secret is found.
-	ErrorMessageMissingHetznerSecret string = "could not find HetznerSecret"
+	ErrorMessageMissingHetznerSecret = "could not find HetznerSecret"
 	// ErrorMessageMissingRescueSSHSecret specifies the error message when no RescueSSH secret is found.
-	ErrorMessageMissingRescueSSHSecret string = "could not find RescueSSHSecret"
+	ErrorMessageMissingRescueSSHSecret = "could not find RescueSSHSecret"
 	// ErrorMessageMissingOSSSHSecret specifies the error message when no OSSSH secret is found.
-	ErrorMessageMissingOSSSHSecret string = "could not find OSSSHSecret"
+	ErrorMessageMissingOSSSHSecret = "could not find OSSSHSecret"
 	// ErrorMessageMissingOrInvalidSecretData specifies the error message when no data in secret is missing or invalid.
-	ErrorMessageMissingOrInvalidSecretData string = "invalid or not specified information in secret"
+	ErrorMessageMissingOrInvalidSecretData = "invalid or not specified information in secret"
 )
 
 // ProvisioningState defines the states of provisioning of the host.
diff --git a/api/v1beta1/hetznerbaremetalmachine_types.go b/api/v1beta1/hetznerbaremetalmachine_types.go
index 42b40760..66e5eff3 100644
--- a/api/v1beta1/hetznerbaremetalmachine_types.go
+++ b/api/v1beta1/hetznerbaremetalmachine_types.go
@@ -29,9 +29,13 @@ import (
 )
 
 const (
-	// BareMetalMachineFinalizer allows Reconcilehetznerbaremetalmachine to clean up resources associated with hetznerbaremetalmachine before
+	// HetznerBareMetalMachineFinalizer allows Reconcilehetznerbaremetalmachine to clean up resources associated with hetznerbaremetalmachine before
 	// removing it from the apiserver.
-	BareMetalMachineFinalizer = "hetznerbaremetalmachine.infrastructure.cluster.x-k8s.io"
+	HetznerBareMetalMachineFinalizer = "infrastructure.cluster.x-k8s.io/hetznerbaremetalmachine"
+
+	// DeprecatedBareMetalMachineFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedBareMetalMachineFinalizer = "hetznerbaremetalmachine.infrastructure.cluster.x-k8s.io"
 
 	// BareMetalHostNamePrefix is a prefix for all hostNames of bare metal servers.
 	BareMetalHostNamePrefix = "bm-"
diff --git a/api/v1beta1/hetznerbaremetalremediation_types.go b/api/v1beta1/hetznerbaremetalremediation_types.go
index a1829588..8fb7967f 100644
--- a/api/v1beta1/hetznerbaremetalremediation_types.go
+++ b/api/v1beta1/hetznerbaremetalremediation_types.go
@@ -21,10 +21,6 @@ import (
 )
 
 const (
-	// RemediationFinalizer allows HetznerBareMetalRemediationReconciler to clean up resources associated with HetznerBareMetalRemediation before
-	// removing it from the apiserver.
-	RemediationFinalizer = "hetznerbaremetalremediation.infrastructure.cluster.x-k8s.io"
-
 	// RebootAnnotation indicates that a bare metal host object should be rebooted.
 	RebootAnnotation = "reboot.hetznerbaremetalhost.infrastructure.cluster.x-k8s.io"
 
diff --git a/api/v1beta1/hetznercluster_types.go b/api/v1beta1/hetznercluster_types.go
index fa99dc64..fbba1f28 100644
--- a/api/v1beta1/hetznercluster_types.go
+++ b/api/v1beta1/hetznercluster_types.go
@@ -22,10 +22,15 @@ import (
 )
 
 const (
-	// ClusterFinalizer allows ReconcileHetznerCluster to clean up HCloud
+	// HetznerClusterFinalizer allows ReconcileHetznerCluster to clean up HCloud
 	// resources associated with HetznerCluster before removing it from the
 	// apiserver.
-	ClusterFinalizer = "hetznercluster.infrastructure.cluster.x-k8s.io"
+	HetznerClusterFinalizer = "infrastructure.cluster.x-k8s.io/hetznercluster"
+
+	// DeprecatedHetznerClusterFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedHetznerClusterFinalizer = "hetznercluster.infrastructure.cluster.x-k8s.io"
+
 	// AllowEmptyControlPlaneAddressAnnotation allows HetznerCluster Webhook
 	// to skip some validation steps for externally managed control planes.
 	AllowEmptyControlPlaneAddressAnnotation = "capi.syself.com/allow-empty-control-plane-address"
diff --git a/controllers/hcloudmachine_controller.go b/controllers/hcloudmachine_controller.go
index 221c8117..fe34ca57 100644
--- a/controllers/hcloudmachine_controller.go
+++ b/controllers/hcloudmachine_controller.go
@@ -187,7 +187,8 @@ func (r *HCloudMachineReconciler) reconcileDelete(ctx context.Context, machineSc
 		return result, nil
 	}
 	// Machine is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.MachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.HCloudMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	return reconcile.Result{}, nil
 }
@@ -196,7 +197,8 @@ func (r *HCloudMachineReconciler) reconcileNormal(ctx context.Context, machineSc
 	hcloudMachine := machineScope.HCloudMachine
 
 	// If the HCloudMachine doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(machineScope.HCloudMachine, infrav1.MachineFinalizer)
+	controllerutil.AddFinalizer(machineScope.HCloudMachine, infrav1.HCloudMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.HCloudMachine, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	// Register the finalizer immediately to avoid orphaning HCloud resources on delete.
 	if err := machineScope.PatchObject(ctx); err != nil {
diff --git a/controllers/hcloudmachinetemplate_controller.go b/controllers/hcloudmachinetemplate_controller.go
index 16258da0..f0d370d0 100644
--- a/controllers/hcloudmachinetemplate_controller.go
+++ b/controllers/hcloudmachinetemplate_controller.go
@@ -78,7 +78,8 @@ func (r *HCloudMachineTemplateReconciler) Reconcile(ctx context.Context, req rec
 	}()
 
 	// removing finalizer that was set in previous versions but is not needed
-	controllerutil.RemoveFinalizer(machineTemplate, infrav1.MachineFinalizer)
+	// We can remove that code in 2025.
+	controllerutil.RemoveFinalizer(machineTemplate, infrav1.DeprecatedHCloudMachineFinalizer)
 
 	// Check whether owner is a ClusterClass. In that case there is nothing to do.
 	if hasOwnerClusterClass(machineTemplate.ObjectMeta) {
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index be069bc2..3b08a72e 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -35,6 +35,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -46,7 +47,6 @@ import (
 	robotclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/robot"
 	sshclient "github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/client/ssh"
 	"github.com/syself/cluster-api-provider-hetzner/pkg/services/baremetal/host"
-	"github.com/syself/cluster-api-provider-hetzner/pkg/utils"
 )
 
 // HetznerBareMetalHostReconciler reconciles a HetznerBareMetalHost object.
@@ -87,12 +87,12 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
 	}
 
 	// Add a finalizer to newly created objects.
-	if bmHost.DeletionTimestamp.IsZero() && !hostHasFinalizer(bmHost) {
-		bmHost.Finalizers = append(bmHost.Finalizers,
-			infrav1.BareMetalHostFinalizer)
+	if bmHost.DeletionTimestamp.IsZero() &&
+		(controllerutil.AddFinalizer(bmHost, infrav1.HetznerBareMetalHostFinalizer) ||
+			controllerutil.RemoveFinalizer(bmHost, infrav1.DeprecatedBareMetalHostFinalizer)) {
 		err := r.Update(ctx, bmHost)
 		if err != nil {
-			return reconcile.Result{}, fmt.Errorf("failed to add finalizer: %w", err)
+			return reconcile.Result{}, fmt.Errorf("failed to update finalizer: %w", err)
 		}
 		return ctrl.Result{Requeue: true}, nil
 	}
@@ -230,7 +230,7 @@ func (r *HetznerBareMetalHostReconciler) reconcileSelectedStates(ctx context.Con
 		if needsUpdate {
 			err := r.Update(ctx, bmHost)
 			if err != nil {
-				return reconcile.Result{}, fmt.Errorf("failed to add finalizer: %w", err)
+				return reconcile.Result{}, fmt.Errorf("Update() failed after setting ProvisioningState: %w", err)
 			}
 		}
 
@@ -238,13 +238,12 @@ func (r *HetznerBareMetalHostReconciler) reconcileSelectedStates(ctx context.Con
 
 		// Handle StateDeleting
 	case infrav1.StateDeleting:
-		if !utils.StringInList(bmHost.Finalizers, infrav1.BareMetalHostFinalizer) {
-			return reconcile.Result{Requeue: true}, nil
-		}
-
-		bmHost.Finalizers = utils.FilterStringFromList(bmHost.Finalizers, infrav1.BareMetalHostFinalizer)
-		if err := r.Update(ctx, bmHost); err != nil {
-			return reconcile.Result{}, fmt.Errorf("failed to remove finalizer: %w", err)
+		if controllerutil.RemoveFinalizer(bmHost, infrav1.HetznerBareMetalHostFinalizer) ||
+			controllerutil.RemoveFinalizer(bmHost, infrav1.DeprecatedBareMetalHostFinalizer) {
+			// at least one finalizer was removed.
+			if err := r.Update(ctx, bmHost); err != nil {
+				return reconcile.Result{}, fmt.Errorf("failed to remove finalizer: %w", err)
+			}
 		}
 		return reconcile.Result{Requeue: true}, nil
 	}
@@ -411,10 +410,6 @@ func hetznerSecretErrorResult(
 	return reconcile.Result{}, fmt.Errorf("hetznerSecretErrorResult: an unhandled failure occurred: %T %w", err, err)
 }
 
-func hostHasFinalizer(host *infrav1.HetznerBareMetalHost) bool {
-	return utils.StringInList(host.Finalizers, infrav1.BareMetalHostFinalizer)
-}
-
 // SetupWithManager sets up the controller with the Manager.
 func (r *HetznerBareMetalHostReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
 	return ctrl.NewControllerManagedBy(mgr).
diff --git a/controllers/hetznerbaremetalmachine_controller.go b/controllers/hetznerbaremetalmachine_controller.go
index 92a96261..fa4de11f 100644
--- a/controllers/hetznerbaremetalmachine_controller.go
+++ b/controllers/hetznerbaremetalmachine_controller.go
@@ -178,14 +178,16 @@ func (r *HetznerBareMetalMachineReconciler) reconcileDelete(ctx context.Context,
 		return result, nil
 	}
 	// Machine is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.BareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.HetznerBareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.DeprecatedBareMetalMachineFinalizer)
 
 	return result, nil
 }
 
 func (r *HetznerBareMetalMachineReconciler) reconcileNormal(ctx context.Context, machineScope *scope.BareMetalMachineScope) (reconcile.Result, error) {
 	// If the HetznerBareMetalMachine doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(machineScope.BareMetalMachine, infrav1.BareMetalMachineFinalizer)
+	controllerutil.AddFinalizer(machineScope.BareMetalMachine, infrav1.HetznerBareMetalMachineFinalizer)
+	controllerutil.RemoveFinalizer(machineScope.BareMetalMachine, infrav1.DeprecatedBareMetalMachineFinalizer)
 
 	// Register the finalizer immediately to avoid orphaning HetznerBareMetal resources on delete
 	if err := machineScope.PatchObject(ctx); err != nil {
diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go
index 905ed3a9..232cb232 100644
--- a/controllers/hetznercluster_controller.go
+++ b/controllers/hetznercluster_controller.go
@@ -178,7 +178,9 @@ func (r *HetznerClusterReconciler) reconcileNormal(ctx context.Context, clusterS
 	hetznerCluster := clusterScope.HetznerCluster
 
 	// If the HetznerCluster doesn't have our finalizer, add it.
-	controllerutil.AddFinalizer(hetznerCluster, infrav1.ClusterFinalizer)
+	controllerutil.AddFinalizer(hetznerCluster, infrav1.HetznerClusterFinalizer)
+	controllerutil.RemoveFinalizer(hetznerCluster, infrav1.DeprecatedHetznerClusterFinalizer)
+
 	if err := clusterScope.PatchObject(ctx); err != nil {
 		return reconcile.Result{}, err
 	}
@@ -366,7 +368,8 @@ func (r *HetznerClusterReconciler) reconcileDelete(ctx context.Context, clusterS
 	}
 
 	// Cluster is deleted so remove the finalizer.
-	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.ClusterFinalizer)
+	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.HetznerClusterFinalizer)
+	controllerutil.RemoveFinalizer(clusterScope.HetznerCluster, infrav1.DeprecatedHetznerClusterFinalizer)
 
 	return reconcile.Result{}, nil
 }
diff --git a/pkg/secrets/secret.go b/pkg/secrets/secret.go
index 46a2d376..656b0a0c 100644
--- a/pkg/secrets/secret.go
+++ b/pkg/secrets/secret.go
@@ -35,7 +35,11 @@ import (
 
 const (
 	// SecretFinalizer is the finalizer for secrets.
-	SecretFinalizer = infrav1.ClusterFinalizer + "/secret"
+	SecretFinalizer = "infrastructure.cluster.x-k8s.io/caph-secret"
+
+	// DeprecatedSecretFinalizer contains the old string.
+	// The controller will automatically update to the new string.
+	DeprecatedSecretFinalizer = infrav1.DeprecatedHetznerClusterFinalizer + "/secret"
 )
 
 // SecretManager is a type for fetching Secrets whether or not they are in the
@@ -91,8 +95,8 @@ func (sm *SecretManager) claimSecret(ctx context.Context, secret *corev1.Secret,
 		}
 	}
 
-	if addFinalizer && !utils.StringInList(secret.Finalizers, SecretFinalizer) {
-		secret.Finalizers = append(secret.Finalizers, SecretFinalizer)
+	if addFinalizer && (controllerutil.AddFinalizer(secret, SecretFinalizer) ||
+		controllerutil.RemoveFinalizer(secret, DeprecatedSecretFinalizer)) {
 		needsUpdate = true
 	}
 
@@ -180,8 +184,8 @@ func (sm *SecretManager) ReleaseSecret(ctx context.Context, secret *corev1.Secre
 
 	// remove finalizer from secret to allow deletion if no other owner exists
 	if !foundOtherHetznerClusterOwner {
-		secret.Finalizers = utils.FilterStringFromList(
-			secret.Finalizers, SecretFinalizer)
+		controllerutil.RemoveFinalizer(secret, SecretFinalizer)
+		controllerutil.RemoveFinalizer(secret, DeprecatedSecretFinalizer)
 	}
 
 	secret.OwnerReferences = newOwnerRefs
diff --git a/pkg/services/baremetal/host/host.go b/pkg/services/baremetal/host/host.go
index 9fac0ebc..feed86e8 100644
--- a/pkg/services/baremetal/host/host.go
+++ b/pkg/services/baremetal/host/host.go
@@ -40,6 +40,7 @@ import (
 	"sigs.k8s.io/cluster-api/util/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
@@ -1803,7 +1804,8 @@ func (s *Service) actionDeprovisioning(_ context.Context) actionResult {
 }
 
 func (s *Service) actionDeleting(_ context.Context) actionResult {
-	s.scope.HetznerBareMetalHost.Finalizers = utils.FilterStringFromList(s.scope.HetznerBareMetalHost.Finalizers, infrav1.BareMetalHostFinalizer)
+	controllerutil.RemoveFinalizer(s.scope.HetznerBareMetalHost, infrav1.HetznerBareMetalHostFinalizer)
+	controllerutil.RemoveFinalizer(s.scope.HetznerBareMetalHost, infrav1.DeprecatedBareMetalHostFinalizer)
 	return deleteComplete{}
 }
 
diff --git a/pkg/services/hcloud/server/server.go b/pkg/services/hcloud/server/server.go
index bce934e0..7911f708 100644
--- a/pkg/services/hcloud/server/server.go
+++ b/pkg/services/hcloud/server/server.go
@@ -189,6 +189,23 @@ func (s *Service) Reconcile(ctx context.Context) (res reconcile.Result, err erro
 	return res, nil
 }
 
+// implements setting rate limit on hcloudmachine.
+func handleRateLimit(hm *infrav1.HCloudMachine, err error, functionName string, errMsg string) error {
+	// returns error if not a rate limit exceeded error
+	if !hcloud.IsError(err, hcloud.ErrorCodeRateLimitExceeded) {
+		return fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	// does not return error if machine is running and does not have a deletion timestamp
+	if hm.Status.Ready && hm.DeletionTimestamp.IsZero() {
+		return nil
+	}
+
+	// check for a rate limit exceeded error if the machine is not running or if machine has a deletion timestamp
+	hcloudutil.HandleRateLimitExceeded(hm, err, functionName)
+	return fmt.Errorf("%s: %w", errMsg, err)
+}
+
 // Delete implements delete method of server.
 func (s *Service) Delete(ctx context.Context) (res reconcile.Result, err error) {
 	server, err := s.findServer(ctx)
@@ -246,12 +263,11 @@ func (s *Service) reconcileNetworkAttachment(ctx context.Context, server *hcloud
 			ID: s.scope.HetznerCluster.Status.Network.ID,
 		},
 	}); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "AttachServerToNetwork")
 		// check if network status is old and server is in fact already attached
 		if hcloud.IsError(err, hcloud.ErrorCodeServerAlreadyAttached) {
 			return nil
 		}
-		return fmt.Errorf("failed to attach server to network: %w", err)
+		return handleRateLimit(s.scope.HCloudMachine, err, "AttachServerToNetwork", "failed to attach server to network")
 	}
 
 	return nil
@@ -308,11 +324,11 @@ func (s *Service) reconcileLoadBalancerAttachment(ctx context.Context, server *h
 	}
 
 	if err := s.scope.HCloudClient.AddTargetServerToLoadBalancer(ctx, opts, loadBalancer); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "AddTargetServerToLoadBalancer")
 		if hcloud.IsError(err, hcloud.ErrorCodeTargetAlreadyDefined) {
 			return reconcile.Result{}, nil
 		}
-		return reconcile.Result{}, fmt.Errorf("failed to add server %s with ID %d as target to load balancer: %w", server.Name, server.ID, err)
+		errMsg := fmt.Sprintf("failed to add server %s with ID %d as target to load balancer", server.Name, server.ID)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "AddTargetServerToLoadBalancer", errMsg)
 	}
 
 	record.Eventf(
@@ -429,8 +445,7 @@ func (s *Service) createServer(ctx context.Context) (*hcloud.Server, error) {
 	// get all ssh keys that are stored in HCloud API
 	sshKeysAPI, err := s.scope.HCloudClient.ListSSHKeys(ctx, hcloud.SSHKeyListOpts{})
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListSSHKeys")
-		return nil, fmt.Errorf("failed listing ssh heys from hcloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListSSHKeys", "failed listing ssh keys from hcloud")
 	}
 
 	// find matching keys and store them
@@ -479,8 +494,8 @@ func (s *Service) createServer(ctx context.Context) (*hcloud.Server, error) {
 			s.scope.Name(),
 			err,
 		)
-		err = errors.Join(errServerCreateNotPossible, err)
-		return nil, fmt.Errorf("failed to create HCloud server %s: %w", s.scope.HCloudMachine.Name, err)
+		errMsg := fmt.Sprintf("failed to create HCloud server %s", s.scope.HCloudMachine.Name)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "CreateServer", errMsg)
 	}
 
 	// set ssh keys to status
@@ -497,8 +512,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 	// Get server type so we can filter for images with correct architecture
 	serverType, err := s.scope.HCloudClient.GetServerType(ctx, string(s.scope.HCloudMachine.Spec.Type))
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "GetServerType")
-		return nil, fmt.Errorf("failed to get server type in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "GetServerType", "failed to get server type in HCloud")
 	}
 	if serverType == nil {
 		conditions.MarkFalse(
@@ -522,8 +536,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 
 	images, err := s.scope.HCloudClient.ListImages(ctx, listOpts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListImages")
-		return nil, fmt.Errorf("failed to list images by label in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListImages", "failed to list images by label in HCloud")
 	}
 
 	// query for an existing image by name.
@@ -533,8 +546,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 	}
 	imagesByName, err := s.scope.HCloudClient.ListImages(ctx, listOpts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListImages")
-		return nil, fmt.Errorf("failed to list images by name in HCloud: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListImages", "failed to list images by name in HCloud")
 	}
 
 	images = append(images, imagesByName...)
@@ -577,12 +589,11 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 		if time.Now().Before(serverAvailableCondition.LastTransitionTime.Time.Add(serverOffTimeout)) {
 			// Not yet timed out, try again to power on
 			if err := s.scope.HCloudClient.PowerOnServer(ctx, server); err != nil {
-				hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "PowerOnServer")
 				if hcloud.IsError(err, hcloud.ErrorCodeLocked) {
 					// if server is locked, we just retry again
 					return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 				}
-				return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
+				return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "PowerOnServer", "failed to power on server")
 			}
 		} else {
 			// Timed out. Set failure reason
@@ -592,12 +603,11 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 	} else {
 		// No condition set yet. Try to power server on.
 		if err := s.scope.HCloudClient.PowerOnServer(ctx, server); err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "PowerOnServer")
 			if hcloud.IsError(err, hcloud.ErrorCodeLocked) {
 				// if server is locked, we just retry again
 				return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 			}
-			return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
+			return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "PowerOnServer", "failed to power on server")
 		}
 		conditions.MarkFalse(
 			s.scope.HCloudMachine,
@@ -619,8 +629,7 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 
 	if s.scope.HasServerAvailableCondition() {
 		if err := s.scope.HCloudClient.ShutdownServer(ctx, server); err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ShutdownServer")
-			return reconcile.Result{}, fmt.Errorf("failed to shutdown server: %w", err)
+			return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "ShutdownServer", "failed to shutdown server")
 		}
 
 		conditions.MarkFalse(s.scope.HCloudMachine,
@@ -635,9 +644,8 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 
 	// timeout for shutdown has been reached - delete server
 	if err := s.scope.HCloudClient.DeleteServer(ctx, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteServer")
 		record.Warnf(s.scope.HCloudMachine, "FailedDeleteHCloudServer", "Failed to delete HCloud server %s", s.scope.Name())
-		return reconcile.Result{}, fmt.Errorf("failed to delete server: %w", err)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "DeleteServer", "failed to delete server")
 	}
 
 	record.Eventf(s.scope.HCloudMachine, "HCloudServerDeleted", "HCloud server %s deleted", s.scope.Name())
@@ -647,9 +655,8 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 func (s *Service) handleDeleteServerStatusOff(ctx context.Context, server *hcloud.Server) (res reconcile.Result, err error) {
 	// server is off and can be deleted
 	if err := s.scope.HCloudClient.DeleteServer(ctx, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteServer")
 		record.Warnf(s.scope.HCloudMachine, "FailedDeleteHCloudServer", "Failed to delete HCloud server %s", s.scope.Name())
-		return reconcile.Result{}, fmt.Errorf("failed to delete server: %w", err)
+		return reconcile.Result{}, handleRateLimit(s.scope.HCloudMachine, err, "DeleteServer", "failed to delete server")
 	}
 
 	record.Eventf(s.scope.HCloudMachine, "HCloudServerDeleted", "HCloud server %s deleted", s.scope.Name())
@@ -660,12 +667,12 @@ func (s *Service) deleteServerOfLoadBalancer(ctx context.Context, server *hcloud
 	lb := &hcloud.LoadBalancer{ID: s.scope.HetznerCluster.Status.ControlPlaneLoadBalancer.ID}
 
 	if err := s.scope.HCloudClient.DeleteTargetServerOfLoadBalancer(ctx, lb, server); err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "DeleteTargetServerOfLoadBalancer")
 		// do not return an error in case the target was not found
 		if strings.Contains(err.Error(), "load_balancer_target_not_found") {
 			return nil
 		}
-		return fmt.Errorf("failed to delete server %s with ID %d as target of load balancer %s with ID %d: %w", server.Name, server.ID, lb.Name, lb.ID, err)
+		errMsg := fmt.Sprintf("failed to delete server %s with ID %d as target of load balancer %s with ID %d", server.Name, server.ID, lb.Name, lb.ID)
+		return handleRateLimit(s.scope.HCloudMachine, err, "DeleteTargetServerOfLoadBalancer", errMsg)
 	}
 	record.Eventf(
 		s.scope.HetznerCluster,
@@ -685,8 +692,8 @@ func (s *Service) findServer(ctx context.Context) (*hcloud.Server, error) {
 	if err == nil {
 		server, err = s.scope.HCloudClient.GetServer(ctx, serverID)
 		if err != nil {
-			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "GetServer")
-			return nil, fmt.Errorf("failed to get server %d: %w", serverID, err)
+			errMsg := fmt.Sprintf("failed to get server %d", serverID)
+			return nil, handleRateLimit(s.scope.HCloudMachine, err, "GetServer", errMsg)
 		}
 
 		// if server has been found, return it
@@ -702,8 +709,7 @@ func (s *Service) findServer(ctx context.Context) (*hcloud.Server, error) {
 
 	servers, err := s.scope.HCloudClient.ListServers(ctx, opts)
 	if err != nil {
-		hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "ListServers")
-		return nil, fmt.Errorf("failed to list servers: %w", err)
+		return nil, handleRateLimit(s.scope.HCloudMachine, err, "ListServers", "failed to list servers")
 	}
 
 	if len(servers) > 1 {
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 7fc84a56..20541a02 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -172,17 +172,6 @@ func StringInList(list []string, strToSearch string) bool {
 	return false
 }
 
-// FilterStringFromList produces a new string slice that does not
-// include the strToFilter argument.
-func FilterStringFromList(list []string, strToFilter string) (newList []string) {
-	for _, item := range list {
-		if item != strToFilter {
-			newList = append(newList, item)
-		}
-	}
-	return
-}
-
 // GenerateName takes a name as string pointer. It returns name if pointer is not nil, otherwise it returns fallback with random suffix.
 func GenerateName(name *string, fallback string) string {
 	if name != nil {


Test patch:
diff --git a/controllers/hcloudremediation_controller_test.go b/controllers/hcloudremediation_controller_test.go
index cde728e5..c1910e11 100644
--- a/controllers/hcloudremediation_controller_test.go
+++ b/controllers/hcloudremediation_controller_test.go
@@ -258,7 +258,7 @@ var _ = Describe("HCloudRemediationReconciler", func() {
 			}, timeout).Should(BeTrue())
 		})
 
-		It("should delete machine if retry limit reached and reboot timed out", func() {
+		It("should delete machine if retry limit reached and reboot timed out (hcloud)", func() {
 			By("creating hcloudRemediation")
 			Expect(testEnv.Create(ctx, hcloudRemediation)).To(Succeed())
 
diff --git a/controllers/hetznerbaremetalhost_controller_test.go b/controllers/hetznerbaremetalhost_controller_test.go
index 326e54f5..9c77fc87 100644
--- a/controllers/hetznerbaremetalhost_controller_test.go
+++ b/controllers/hetznerbaremetalhost_controller_test.go
@@ -226,7 +226,7 @@ var _ = Describe("HetznerBareMetalHostReconciler", func() {
 					return false
 				}
 				for _, finalizer := range host.GetFinalizers() {
-					if finalizer == infrav1.BareMetalHostFinalizer {
+					if finalizer == infrav1.HetznerBareMetalHostFinalizer {
 						return true
 					}
 				}
diff --git a/controllers/hetznerbaremetalmachine_controller_test.go b/controllers/hetznerbaremetalmachine_controller_test.go
index b3769b6a..1f10f680 100644
--- a/controllers/hetznerbaremetalmachine_controller_test.go
+++ b/controllers/hetznerbaremetalmachine_controller_test.go
@@ -336,7 +336,7 @@ var _ = Describe("HetznerBareMetalMachineReconciler", func() {
 						return false
 					}
 					for _, finalizer := range bmMachine.GetFinalizers() {
-						if finalizer == infrav1.BareMetalMachineFinalizer {
+						if finalizer == infrav1.HetznerBareMetalMachineFinalizer {
 							return true
 						}
 					}
diff --git a/controllers/hetznerbaremetalremediation_controller_test.go b/controllers/hetznerbaremetalremediation_controller_test.go
index e4364119..fd2aa54f 100644
--- a/controllers/hetznerbaremetalremediation_controller_test.go
+++ b/controllers/hetznerbaremetalremediation_controller_test.go
@@ -358,7 +358,7 @@ var _ = Describe("HetznerBareMetalRemediationReconciler", func() {
 					}, timeout).Should(BeTrue())
 				})
 
-				It("should delete machine if retry limit reached and reboot timed out", func() {
+				It("should delete machine if retry limit reached and reboot timed out (bm)", func() {
 					By("creating hetznerBareMetalRemediation object")
 					Expect(testEnv.Create(ctx, hetznerBareMetalRemediation)).To(Succeed())
 
diff --git a/pkg/services/hcloud/server/server_test.go b/pkg/services/hcloud/server/server_test.go
index d7e2fc33..8f8d89fa 100644
--- a/pkg/services/hcloud/server/server_test.go
+++ b/pkg/services/hcloud/server/server_test.go
@@ -18,11 +18,13 @@ package server
 
 import (
 	"context"
+	"fmt"
 	"time"
 
 	"github.com/hetznercloud/hcloud-go/v2/hcloud"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -339,3 +341,105 @@ var _ = Describe("Test ValidateLabels", func() {
 		}),
 	)
 })
+
+var _ = Describe("Test handleRateLimit", func() {
+	type testCaseHandleRateLimit struct {
+		hm              *infrav1.HCloudMachine
+		err             error
+		functionName    string
+		errMsg          string
+		expectError     error
+		expectCondition bool
+	}
+
+	DescribeTable("Test handleRateLimit",
+		func(tc testCaseHandleRateLimit) {
+			err := handleRateLimit(tc.hm, tc.err, tc.functionName, tc.errMsg)
+			if tc.expectError != nil {
+				Expect(err).To(MatchError(tc.expectError))
+			} else {
+				Expect(err).To(BeNil())
+			}
+			if tc.expectCondition {
+				Expect(isPresentAndFalseWithReason(tc.hm, infrav1.HetznerAPIReachableCondition, infrav1.RateLimitExceededReason)).To(BeTrue())
+			} else {
+				Expect(conditions.Get(tc.hm, infrav1.HetznerAPIReachableCondition)).To(BeNil())
+			}
+		},
+		Entry("machine not ready, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine has deletion timestamp, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				ObjectMeta: metav1.ObjectMeta{
+					DeletionTimestamp: &metav1.Time{Time: time.Now()},
+				},
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine not ready, has deletion timestamp, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				ObjectMeta: metav1.ObjectMeta{
+					DeletionTimestamp: &metav1.Time{Time: time.Now()},
+				},
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded}),
+			expectCondition: true,
+		}),
+		Entry("machine ready, rate limit exceeded error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeRateLimitExceeded},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     nil,
+			expectCondition: false,
+		}),
+		Entry("machine ready, other error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: true},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeResourceUnavailable},
+			functionName:    "TestFunction",
+			errMsg:          "Test error message",
+			expectError:     fmt.Errorf("Test error message: %w", hcloud.Error{Code: hcloud.ErrorCodeResourceUnavailable}),
+			expectCondition: false,
+		}),
+		Entry("machine not ready, other error", testCaseHandleRateLimit{
+			hm: &infrav1.HCloudMachine{
+				Status: infrav1.HCloudMachineStatus{Ready: false},
+			},
+			err:             hcloud.Error{Code: hcloud.ErrorCodeConflict},
+			functionName:    "TestFunction",
+			errMsg:          "Test conflict error message",
+			expectError:     fmt.Errorf("Test conflict error message: %w", hcloud.Error{Code: hcloud.ErrorCodeConflict}),
+			expectCondition: false,
+		}),
+	)
+})
+
+func isPresentAndFalseWithReason(getter conditions.Getter, condition clusterv1.ConditionType, reason string) bool {
+	if !conditions.Has(getter, condition) {
+		return false
+	}
+	objectCondition := conditions.Get(getter, condition)
+	return objectCondition.Status == corev1.ConditionFalse &&
+		objectCondition.Reason == reason
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 0e09f4f8..742debe7 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -177,29 +177,6 @@ var _ = Describe("StringInList", func() {
 		}))
 })
 
-var _ = Describe("FilterStringFromList", func() {
-	type testCaseFilterStringFromList struct {
-		list            []string
-		str             string
-		expectedOutcome []string
-	}
-	DescribeTable("Test filter string from list",
-		func(tc testCaseFilterStringFromList) {
-			out := utils.FilterStringFromList(tc.list, tc.str)
-			Expect(out).To(Equal(tc.expectedOutcome))
-		},
-		Entry("entry1", testCaseFilterStringFromList{
-			list:            []string{"a", "b", "c"},
-			str:             "a",
-			expectedOutcome: []string{"b", "c"},
-		}),
-		Entry("entry2", testCaseFilterStringFromList{
-			list:            []string{"a", "b", "c"},
-			str:             "d",
-			expectedOutcome: []string{"a", "b", "c"},
-		}))
-})
-
 var _ = Describe("Test removeOwnerRefFromList", func() {
 	type testCaseRemoveOwnerRefFromList struct {
 		RefList         []metav1.OwnerReference