Repository: syself/cluster-api-provider-hetzner
Pull request #1358 (2024-06-21 11:01:54)

:seedling: Update image docker.io/library/alpine to v3.20.1
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| docker.io/library/alpine | final | patch | `3.20.0` -> `3.20.1` |
| docker.io/library/alpine | stage | patch | `3.20.0` -> `3.20.1` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MTMuNCIsInVwZGF0ZWRJblZlciI6IjM3LjQxMy40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJ0eXBlL3BhdGNoIiwidXBkYXRlL2NvbnRhaW5lciJdfQ==-->



Patch:
diff --git a/controllers/csr_controller.go b/controllers/csr_controller.go
index 0b6bf8e4..cb80895f 100644
--- a/controllers/csr_controller.go
+++ b/controllers/csr_controller.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"crypto/x509"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"regexp"
 	"strings"
@@ -108,6 +109,11 @@ func (r *GuestCSRReconciler) Reconcile(ctx context.Context, req reconcile.Reques
 	} else {
 		// get machine addresses from corresponding machine
 		machineAddresses, isHCloudMachine, err := r.getMachineAddresses(ctx, certificateSigningRequest)
+		if errors.Is(err, errNoHetznerBareMetalMachineByProviderIDFound) {
+			log.Info(fmt.Sprintf("ProviderID not set yet. The hbmm seems to be in 'ensure-provision'. Retrying. %s",
+				err.Error()))
+			return reconcile.Result{RequeueAfter: 5 * time.Second}, nil
+		}
 		if err != nil {
 			log.Error(err, "could not find an associated bm machine or hcloud machine",
 				"userName", certificateSigningRequest.Spec.Username)
@@ -200,34 +206,50 @@ func machineNameWithPrefix(machineName string, isHCloudMachine bool) string {
 	return hostNamePrefix + machineName
 }
 
-var constantBareMetalHostnameRegex = regexp.MustCompile(`^bm-(\S*)-(\d+)$`)
+var (
+	constantBareMetalHostnameRegex                = regexp.MustCompile(`^bm-(\S*)-(\d+)$`)
+	errNoHetznerBareMetalMachineByProviderIDFound = fmt.Errorf("no HetznerBaremetalMachine by ProviderID found")
+)
 
 func (r *GuestCSRReconciler) getMachineAddresses(
 	ctx context.Context,
 	certificateSigningRequest *certificatesv1.CertificateSigningRequest,
 ) (machineAddresses []clusterv1.MachineAddress, isHCloudMachine bool, err error) {
-	// try to find matching HCloudMachine object
-	var hcloudMachine infrav1.HCloudMachine
+	log := ctrl.LoggerFrom(ctx)
 
-	hcloudMachineName := types.NamespacedName{
-		Namespace: r.mCluster.Namespace(),
-		Name:      hcloudMachineNameFromCSR(certificateSigningRequest),
-	}
-	err = r.mCluster.Get(ctx, hcloudMachineName, &hcloudMachine)
-	if err != nil {
-		// Could not find HCloud machine. Try to find bare metal machine.
+	_, serverID := getServerIDFromConstantHostname(ctx, certificateSigningRequest.Spec.Username, r.clusterName)
+	if serverID != "" {
+		// According the the regex this is BM server with ConstantHostname. Handle that fist,
+		// no need to check for a HCloud server.
 
-		// We don't know whether the HetznerBareMetalMachine uses a constant hostname or not. If it does, we'll find it with getHbmmWithConstantHostname.
-		// If we don't find it, then it does not use a constant hostname and we'll find it in another way.
 		hbmm, err := getHbmmWithConstantHostname(ctx, certificateSigningRequest.Spec.Username, r.clusterName, r.mCluster)
+		if errors.Is(err, errNoHetznerBareMetalMachineByProviderIDFound) {
+			// No machine found yet. Likely: Cloud-init has run, the kubelet has started. But machine is still in ensure-provisioned.
+			// The providerID will be set soon.
+			return nil, false, err
+		}
 		if err != nil {
 			return nil, false, fmt.Errorf("getHbmmWithConstantHostname(%q) failed: %w", certificateSigningRequest.Spec.Username, err)
 		}
 		if hbmm != nil {
+			log.Info(fmt.Sprintf("found hbmm for %s (ConstantHostname)", certificateSigningRequest.Spec.Username))
 			return hbmm.Status.Addresses, false, nil
 		}
+		return nil, false, fmt.Errorf("getHbmmWithConstantHostname(%q) failed to get hbmm (should not happen)", certificateSigningRequest.Spec.Username)
+	}
 
-		// HetznerBareMetalMachine does not have a constant hostname - use the following method to find it
+	// It could be both: A hcloud machine or a bm-machine without ConstantHostname.
+
+	// Try to find matching HCloudMachine object
+	var hcloudMachine infrav1.HCloudMachine
+
+	hcloudMachineName := types.NamespacedName{
+		Namespace: r.mCluster.Namespace(),
+		Name:      hcloudMachineNameFromCSR(certificateSigningRequest),
+	}
+	err = r.mCluster.Get(ctx, hcloudMachineName, &hcloudMachine)
+	if err != nil {
+		// Could not find HCloud machine. Try to find bare metal machine without ConstantHostname.
 
 		var bmMachine infrav1.HetznerBareMetalMachine
 		bmMachineName := types.NamespacedName{
@@ -241,10 +263,10 @@ func (r *GuestCSRReconciler) getMachineAddresses(
 				bmMachineName.Name,
 				err)
 		}
-
+		log.Info(fmt.Sprintf("found hbmm for %s (no ConstantHostname)", certificateSigningRequest.Spec.Username))
 		return bmMachine.Status.Addresses, false, nil
 	}
-
+	log.Info(fmt.Sprintf("found hcloudmachine for %s", certificateSigningRequest.Spec.Username))
 	return hcloudMachine.Status.Addresses, true, nil
 }
 
@@ -283,24 +305,29 @@ func (r *GuestCSRReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Mana
 		Complete(r)
 }
 
-func getHbmmWithConstantHostname(ctx context.Context, csrUsername string, clusterName string, mCluster ManagementCluster) (*infrav1.HetznerBareMetalMachine, error) {
+func getServerIDFromConstantHostname(ctx context.Context, csrUsername string, clusterName string) (clusterFromCSR string, serverID string) {
 	log := ctrl.LoggerFrom(ctx)
-
 	// example csrUsername: system:node:bm-my-cluster-1234567
 	matches := constantBareMetalHostnameRegex.FindStringSubmatch(strings.TrimPrefix(csrUsername, nodePrefix))
 	if len(matches) != 3 {
 		log.V(1).Info("No constant baremetal hostname - regex does not match CSR username",
 			"regex", constantBareMetalHostnameRegex.String(), "csrUserName", csrUsername)
-		return nil, nil
+		return "", ""
 	}
 
-	clusterFromCSR := matches[1]
+	clusterFromCSR = matches[1]
 	if clusterFromCSR != clusterName {
 		log.V(1).Info("No constant baremetal hostname - mismatch of cluster found in csrUserName", "got", clusterFromCSR, "want", clusterName)
-		return nil, nil
+		return "", ""
 	}
+	return clusterFromCSR, matches[2]
+}
+
+func getHbmmWithConstantHostname(ctx context.Context, csrUsername string, clusterName string, mCluster ManagementCluster) (*infrav1.HetznerBareMetalMachine, error) {
+	log := ctrl.LoggerFrom(ctx)
 
-	providerID := "hcloud://bm-" + matches[2]
+	clusterFromCSR, serverID := getServerIDFromConstantHostname(ctx, csrUsername, clusterName)
+	providerID := "hcloud://bm-" + serverID
 	hList := &infrav1.HetznerBareMetalMachineList{}
 	selector := labels.NewSelector()
 	req, err := labels.NewRequirement(clusterv1.ClusterNameLabel, selection.Equals, []string{clusterFromCSR})
@@ -329,8 +356,7 @@ func getHbmmWithConstantHostname(ctx context.Context, csrUsername string, cluste
 		}
 	}
 	if hbmm == nil {
-		log.V(1).Info("no constant baremetal hostname - did not find HetznerBareMetalMachine by ProviderID", "providerID", providerID)
-		return nil, nil
+		return nil, fmt.Errorf("ProviderID: %q %w", providerID, errNoHetznerBareMetalMachineByProviderIDFound)
 	}
 
 	log.Info("Found HetznerBareMetalMachine with constant hostname", "csr-username", csrUsername, "hetznerBareMetalMachine", hbmm.Name)
diff --git a/controllers/hcloudmachine_controller.go b/controllers/hcloudmachine_controller.go
index d3c1716e..221c8117 100644
--- a/controllers/hcloudmachine_controller.go
+++ b/controllers/hcloudmachine_controller.go
@@ -220,14 +220,16 @@ func (r *HCloudMachineReconciler) SetupWithManager(ctx context.Context, mgr ctrl
 		WithOptions(options).
 		For(&infrav1.HCloudMachine{}).
 		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(log, r.WatchFilterValue)).
+		WithEventFilter(IgnoreInsignificantHCloudMachineStatusUpdates(log)).
 		Watches(
 			&clusterv1.Machine{},
 			handler.EnqueueRequestsFromMapFunc(util.MachineToInfrastructureMapFunc(infrav1.GroupVersion.WithKind("HCloudMachine"))),
+			builder.WithPredicates(IgnoreInsignificantMachineStatusUpdates(log)),
 		).
 		Watches(
 			&infrav1.HetznerCluster{},
 			handler.EnqueueRequestsFromMapFunc(r.HetznerClusterToHCloudMachines(ctx)),
-			builder.WithPredicates(IgnoreHetznerClusterConditionUpdates(log)),
+			builder.WithPredicates(IgnoreInsignificantHetznerClusterUpdates(log)),
 		).
 		Build(r)
 	if err != nil {
@@ -303,8 +305,8 @@ func (r *HCloudMachineReconciler) HetznerClusterToHCloudMachines(_ context.Conte
 	}
 }
 
-// IgnoreHetznerClusterConditionUpdates is a predicate used for ignoring HetznerCluster condition updates.
-func IgnoreHetznerClusterConditionUpdates(logger logr.Logger) predicate.Funcs {
+// IgnoreInsignificantHetznerClusterUpdates is a predicate used for ignoring insignificant HetznerCluster.Status updates.
+func IgnoreInsignificantHetznerClusterUpdates(logger logr.Logger) predicate.Funcs {
 	return predicate.Funcs{
 		UpdateFunc: func(e event.UpdateEvent) bool {
 			log := logger.WithValues(
@@ -341,6 +343,23 @@ func IgnoreHetznerClusterConditionUpdates(logger logr.Logger) predicate.Funcs {
 			oldCluster.Status.Conditions = nil
 			newCluster.Status.Conditions = nil
 
+			if oldCluster.Status.ControlPlaneLoadBalancer != nil {
+				oldCluster.Status.ControlPlaneLoadBalancer.Target = nil
+			}
+			if newCluster.Status.ControlPlaneLoadBalancer != nil {
+				newCluster.Status.ControlPlaneLoadBalancer.Target = nil
+			}
+
+			oldCluster.Status.HCloudPlacementGroups = nil
+			newCluster.Status.HCloudPlacementGroups = nil
+
+			if oldCluster.Status.Network != nil {
+				oldCluster.Status.Network.AttachedServers = nil
+			}
+			if newCluster.Status.Network != nil {
+				newCluster.Status.Network.AttachedServers = nil
+			}
+
 			if reflect.DeepEqual(oldCluster, newCluster) {
 				// Only insignificant fields changed, no need to reconcile
 				return false
@@ -355,3 +374,109 @@ func IgnoreHetznerClusterConditionUpdates(logger logr.Logger) predicate.Funcs {
 		GenericFunc: func(_ event.GenericEvent) bool { return true },
 	}
 }
+
+// IgnoreInsignificantMachineStatusUpdates is a predicate used for ignoring insignificant Machine.Status updates.
+func IgnoreInsignificantMachineStatusUpdates(logger logr.Logger) predicate.Funcs {
+	return predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			log := logger.WithValues(
+				"predicate", "IgnoreInsignificantMachineStatusUpdates",
+				"type", "update",
+				"namespace", e.ObjectNew.GetNamespace(),
+				"kind", strings.ToLower(e.ObjectNew.GetObjectKind().GroupVersionKind().Kind),
+				"name", e.ObjectNew.GetName(),
+			)
+
+			var oldMachine, newMachine *clusterv1.Machine
+			var ok bool
+			// This predicate only looks at Machine objects
+			if oldMachine, ok = e.ObjectOld.(*clusterv1.Machine); !ok {
+				return true
+			}
+			if newMachine, ok = e.ObjectNew.(*clusterv1.Machine); !ok {
+				// Something weird happened, and we received two different kinds of objects
+				return true
+			}
+
+			// We should not modify the original objects, this causes issues with code that relies on the original object.
+			oldMachine = oldMachine.DeepCopy()
+			newMachine = newMachine.DeepCopy()
+
+			oldMachine.ManagedFields = nil
+			newMachine.ManagedFields = nil
+
+			oldMachine.ResourceVersion = ""
+			newMachine.ResourceVersion = ""
+
+			oldMachine.Status = clusterv1.MachineStatus{}
+			newMachine.Status = clusterv1.MachineStatus{}
+
+			if reflect.DeepEqual(oldMachine, newMachine) {
+				// Only insignificant fields changed, no need to reconcile
+				return false
+			}
+
+			log.V(1).Info("Machine -> HCloudMachine event")
+			return true
+		},
+		CreateFunc:  func(_ event.CreateEvent) bool { return true },
+		DeleteFunc:  func(_ event.DeleteEvent) bool { return true },
+		GenericFunc: func(_ event.GenericEvent) bool { return true },
+	}
+}
+
+// IgnoreInsignificantHCloudMachineStatusUpdates is a predicate used for ignoring insignificant HCloudMachine.Status updates.
+func IgnoreInsignificantHCloudMachineStatusUpdates(logger logr.Logger) predicate.Funcs {
+	return predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			log := logger.WithValues(
+				"predicate", "IgnoreInsignificantHCloudMachineStatusUpdates",
+				"type", "update",
+				"namespace", e.ObjectNew.GetNamespace(),
+				"kind", strings.ToLower(e.ObjectNew.GetObjectKind().GroupVersionKind().Kind),
+				"name", e.ObjectNew.GetName(),
+			)
+
+			var oldHCloudMachine, newHCloudMachine *infrav1.HCloudMachine
+			var ok bool
+			// This predicate only looks at HCloudMachine objects
+			if oldHCloudMachine, ok = e.ObjectOld.(*infrav1.HCloudMachine); !ok {
+				return true
+			}
+			if newHCloudMachine, ok = e.ObjectNew.(*infrav1.HCloudMachine); !ok {
+				// Something weird happened, and we received two different kinds of objects
+				return true
+			}
+
+			// We should not modify the original objects, this causes issues with code that relies on the original object.
+			oldHCloudMachine = oldHCloudMachine.DeepCopy()
+			newHCloudMachine = newHCloudMachine.DeepCopy()
+
+			// check if status is empty - if so, it should be restored
+			emptyStatus := infrav1.HCloudMachineStatus{}
+			if reflect.DeepEqual(newHCloudMachine.Status, emptyStatus) {
+				return true
+			}
+
+			oldHCloudMachine.ManagedFields = nil
+			newHCloudMachine.ManagedFields = nil
+
+			oldHCloudMachine.ResourceVersion = ""
+			newHCloudMachine.ResourceVersion = ""
+
+			oldHCloudMachine.Status = infrav1.HCloudMachineStatus{}
+			newHCloudMachine.Status = infrav1.HCloudMachineStatus{}
+
+			if reflect.DeepEqual(oldHCloudMachine, newHCloudMachine) {
+				// Only insignificant fields changed, no need to reconcile
+				return false
+			}
+
+			log.V(1).Info("HCloudMachine event")
+			return true
+		},
+		CreateFunc:  func(_ event.CreateEvent) bool { return true },
+		DeleteFunc:  func(_ event.DeleteEvent) bool { return true },
+		GenericFunc: func(_ event.GenericEvent) bool { return true },
+	}
+}
diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go
index 6c400066..905ed3a9 100644
--- a/controllers/hetznercluster_controller.go
+++ b/controllers/hetznercluster_controller.go
@@ -21,11 +21,13 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"reflect"
 	"strconv"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/go-logr/logr"
 	certificatesv1 "k8s.io/api/certificates/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -46,9 +48,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 	"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/handler"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 
@@ -732,6 +736,7 @@ func (r *HetznerClusterReconciler) SetupWithManager(ctx context.Context, mgr ctr
 		For(&infrav1.HetznerCluster{}).
 		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(log, r.WatchFilterValue)).
 		WithEventFilter(predicates.ResourceIsNotExternallyManaged(log)).
+		WithEventFilter(IgnoreInsignificantHetznerClusterStatusUpdates(log)).
 		Owns(&corev1.Secret{}).
 		Build(r)
 	if err != nil {
@@ -777,5 +782,116 @@ func (r *HetznerClusterReconciler) SetupWithManager(ctx context.Context, mgr ctr
 				},
 			}
 		}),
+		IgnoreInsignificantClusterStatusUpdates(log),
 	)
 }
+
+// IgnoreInsignificantClusterStatusUpdates is a predicate used for ignoring insignificant HetznerCluster.Status updates.
+func IgnoreInsignificantClusterStatusUpdates(logger logr.Logger) predicate.Funcs {
+	return predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			log := logger.WithValues(
+				"predicate", "IgnoreInsignificantClusterStatusUpdates",
+				"type", "update",
+				"namespace", e.ObjectNew.GetNamespace(),
+				"kind", strings.ToLower(e.ObjectNew.GetObjectKind().GroupVersionKind().Kind),
+				"name", e.ObjectNew.GetName(),
+			)
+
+			var oldCluster, newCluster *clusterv1.Cluster
+			var ok bool
+			// This predicate only looks at Cluster objects
+			if oldCluster, ok = e.ObjectOld.(*clusterv1.Cluster); !ok {
+				return true
+			}
+			if newCluster, ok = e.ObjectNew.(*clusterv1.Cluster); !ok {
+				// Something weird happened, and we received two different kinds of objects
+				return true
+			}
+
+			// We should not modify the original objects, this causes issues with code that relies on the original object.
+			oldCluster = oldCluster.DeepCopy()
+			newCluster = newCluster.DeepCopy()
+
+			// Set fields we do not care about to nil
+
+			oldCluster.ManagedFields = nil
+			newCluster.ManagedFields = nil
+
+			oldCluster.ResourceVersion = ""
+			newCluster.ResourceVersion = ""
+
+			oldCluster.Status = clusterv1.ClusterStatus{}
+			newCluster.Status = clusterv1.ClusterStatus{}
+
+			if reflect.DeepEqual(oldCluster, newCluster) {
+				// Only insignificant fields changed, no need to reconcile
+				return false
+			}
+			// There is a noteworthy diff, so we should reconcile
+			log.V(1).Info("Cluster -> HetznerCluster")
+			return true
+		},
+		CreateFunc:  func(_ event.CreateEvent) bool { return true },
+		DeleteFunc:  func(_ event.DeleteEvent) bool { return true },
+		GenericFunc: func(_ event.GenericEvent) bool { return true },
+	}
+}
+
+// IgnoreInsignificantHetznerClusterStatusUpdates is a predicate used for ignoring insignificant HetznerHetznerCluster.Status updates.
+func IgnoreInsignificantHetznerClusterStatusUpdates(logger logr.Logger) predicate.Funcs {
+	return predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			log := logger.WithValues(
+				"predicate", "IgnoreInsignificantHetznerClusterStatusUpdates",
+				"namespace", e.ObjectNew.GetNamespace(),
+				"kind", strings.ToLower(e.ObjectNew.GetObjectKind().GroupVersionKind().Kind),
+				"name", e.ObjectNew.GetName(),
+			)
+
+			var oldHetznerCluster, newHetznerCluster *infrav1.HetznerCluster
+			var ok bool
+			// This predicate only looks at HetznerCluster objects
+			if oldHetznerCluster, ok = e.ObjectOld.(*infrav1.HetznerCluster); !ok {
+				return true
+			}
+			if newHetznerCluster, ok = e.ObjectNew.(*infrav1.HetznerCluster); !ok {
+				// Something weird happened, and we received two different kinds of objects
+				return true
+			}
+
+			// We should not modify the original objects, this causes issues with code that relies on the original object.
+			oldHetznerCluster = oldHetznerCluster.DeepCopy()
+			newHetznerCluster = newHetznerCluster.DeepCopy()
+
+			// check if status is empty - if so, it should be restored
+			emptyStatus := infrav1.HetznerClusterStatus{}
+			if reflect.DeepEqual(newHetznerCluster.Status, emptyStatus) {
+				return true
+			}
+
+			// Set fields we do not care about to nil
+
+			oldHetznerCluster.ManagedFields = nil
+			newHetznerCluster.ManagedFields = nil
+
+			oldHetznerCluster.ResourceVersion = ""
+			newHetznerCluster.ResourceVersion = ""
+
+			oldHetznerCluster.Status = infrav1.HetznerClusterStatus{}
+			newHetznerCluster.Status = infrav1.HetznerClusterStatus{}
+
+			if reflect.DeepEqual(oldHetznerCluster, newHetznerCluster) {
+				// Only insignificant fields changed, no need to reconcile
+				return false
+			}
+			// There is a noteworthy diff, so we should reconcile
+			log.V(1).Info("HetznerCluster Update")
+			return true
+		},
+		// We only care about Update events, anything else should be reconciled
+		CreateFunc:  func(_ event.CreateEvent) bool { return true },
+		DeleteFunc:  func(_ event.DeleteEvent) bool { return true },
+		GenericFunc: func(_ event.GenericEvent) bool { return true },
+	}
+}
diff --git a/images/cache/Dockerfile b/images/cache/Dockerfile
index e15a81f8..fb836243 100644
--- a/images/cache/Dockerfile
+++ b/images/cache/Dockerfile
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 
-FROM docker.io/library/alpine:3.20.0@sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd as import-cache
+FROM docker.io/library/alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 as import-cache
 
 RUN --mount=type=bind,target=/host-tmp \
     --mount=type=cache,target=/root/.cache \
@@ -27,7 +27,7 @@ RUN --mount=type=bind,target=/host-tmp \
       tar xzf /host-tmp/go-pkg-cache.tar.gz --no-same-owner -C /go/pkg; \
     fi
 
-FROM docker.io/library/alpine:3.20.0@sha256:77726ef6b57ddf65bb551896826ec38bc3e53f75cdde31354fbffb4f25238ebd as cache-creator
+FROM docker.io/library/alpine:3.20.1@sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0 as cache-creator
 RUN --mount=type=cache,target=/root/.cache \
     --mount=type=cache,target=/go/pkg \
     tar czf /tmp/go-build-cache.tar.gz -C /root/.cache/go-build . ; \
diff --git a/pkg/services/hcloud/server/server.go b/pkg/services/hcloud/server/server.go
index 9b9d35c4..c42fe286 100644
--- a/pkg/services/hcloud/server/server.go
+++ b/pkg/services/hcloud/server/server.go
@@ -141,12 +141,12 @@ func (s *Service) Reconcile(ctx context.Context) (res reconcile.Result, err erro
 			clusterv1.ConditionSeverityInfo,
 			"server is starting",
 		)
-		return reconcile.Result{RequeueAfter: 10 * time.Second}, nil
+		return reconcile.Result{RequeueAfter: 1 * time.Minute}, nil
 	case hcloud.ServerStatusRunning: // do nothing
 	default:
 		// some temporary status
 		s.scope.SetReady(false)
-		return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
+		return reconcile.Result{RequeueAfter: 10 * time.Second}, nil
 	}
 
 	// check whether server is attached to the network
@@ -296,7 +296,7 @@ func (s *Service) reconcileLoadBalancerAttachment(ctx context.Context, server *h
 	// we attach only nodes with kube-apiserver pod healthy to avoid downtime, skipped for the first node
 	if len(s.scope.HetznerCluster.Status.ControlPlaneLoadBalancer.Target) > 0 &&
 		!conditions.IsTrue(s.scope.Machine, controlplanev1.MachineAPIServerPodHealthyCondition) {
-		return reconcile.Result{RequeueAfter: 5 * time.Second}, nil
+		return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 	}
 
 	opts := hcloud.LoadBalancerAddServerTargetOpts{
@@ -318,8 +318,8 @@ func (s *Service) reconcileLoadBalancerAttachment(ctx context.Context, server *h
 	record.Eventf(
 		s.scope.HetznerCluster,
 		"AddedAsTargetToLoadBalancer",
-		"Added new server %s with ID %d to the loadbalancer %s with ID %d",
-		server.Name, server.ID, s.scope.HetznerCluster.Spec.ControlPlaneLoadBalancer.Name, s.scope.HetznerCluster.Status.ControlPlaneLoadBalancer.ID)
+		"Added new server %s with ID %d to the loadbalancer with ID %d",
+		server.Name, server.ID, s.scope.HetznerCluster.Status.ControlPlaneLoadBalancer.ID)
 
 	return reconcile.Result{}, nil
 }
@@ -499,7 +499,7 @@ func (s *Service) getServerImage(ctx context.Context) (*hcloud.Image, error) {
 			infrav1.ServerCreateSucceededCondition,
 			infrav1.ServerTypeNotFoundReason,
 			clusterv1.ConditionSeverityError,
-			err.Error(),
+			"failed to get server type - nil type",
 		)
 		return nil, errServerCreateNotPossible
 	}
@@ -566,13 +566,14 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 	if serverAvailableCondition != nil &&
 		serverAvailableCondition.Status == corev1.ConditionFalse &&
 		serverAvailableCondition.Reason == infrav1.ServerOffReason {
+		s.scope.Info("Trigger power on again")
 		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{Requeue: true}, nil
+					return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 				}
 				return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
 			}
@@ -587,7 +588,7 @@ func (s *Service) handleServerStatusOff(ctx context.Context, server *hcloud.Serv
 			hcloudutil.HandleRateLimitExceeded(s.scope.HCloudMachine, err, "PowerOnServer")
 			if hcloud.IsError(err, hcloud.ErrorCodeLocked) {
 				// if server is locked, we just retry again
-				return reconcile.Result{Requeue: true}, nil
+				return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 			}
 			return reconcile.Result{}, fmt.Errorf("failed to power on server: %w", err)
 		}
@@ -609,7 +610,7 @@ func (s *Service) handleDeleteServerStatusRunning(ctx context.Context, server *h
 	// 1. The server has not yet been tried to shut down and still is marked as "ready".
 	// 2. The server has been tried to shut down without an effect and the timeout is not reached yet.
 
-	if s.scope.HasServerAvailableCondition() || (s.scope.HasServerTerminatedCondition() && !s.scope.HasShutdownTimedOut()) {
+	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)


Test patch:
diff --git a/controllers/hcloudmachine_controller_test.go b/controllers/hcloudmachine_controller_test.go
index bbbe5553..1ebd8fcf 100644
--- a/controllers/hcloudmachine_controller_test.go
+++ b/controllers/hcloudmachine_controller_test.go
@@ -627,7 +627,7 @@ var _ = Describe("HCloudMachine validation", func() {
 	})
 })
 
-var _ = Describe("IgnoreHetznerClusterConditionUpdates Predicate", func() {
+var _ = Describe("IgnoreInsignificantHetznerClusterUpdates Predicate", func() {
 	var (
 		predicate predicate.Predicate
 
@@ -636,7 +636,7 @@ var _ = Describe("IgnoreHetznerClusterConditionUpdates Predicate", func() {
 	)
 
 	BeforeEach(func() {
-		predicate = IgnoreHetznerClusterConditionUpdates(klog.Background())
+		predicate = IgnoreInsignificantHetznerClusterUpdates(klog.Background())
 
 		oldCluster = &infrav1.HetznerCluster{
 			ObjectMeta: metav1.ObjectMeta{Name: "test-predicate", ResourceVersion: "1"},
diff --git a/test/e2e/log_collector.go b/test/e2e/log_collector.go
index 1feafaf7..29ebfffd 100644
--- a/test/e2e/log_collector.go
+++ b/test/e2e/log_collector.go
@@ -89,7 +89,7 @@ func (collector logCollector) CollectMachineLog(_ context.Context, _ client.Clie
 				return fmt.Errorf("failed to tar dir %s: %w", pathToDir, err)
 			}
 
-			err = os.MkdirAll(outputDir, os.ModePerm) //nolint:gosec //nolint:ignore
+			err = os.MkdirAll(outputDir, 0o750)
 			if err != nil {
 				return err
 			}
@@ -122,7 +122,7 @@ func (collector logCollector) CollectMachineLog(_ context.Context, _ client.Clie
 }
 
 func createOutputFile(path string) (*os.File, error) {
-	if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { //nolint:gosec //nolint:ignore
+	if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
 		return nil, err
 	}
 	return os.Create(path) // #nosec