Repository: syself/cluster-api-provider-hetzner
Pull request #923 (2023-09-19 13:33:53)

✨ Add flag for HCloud rate limitting
<!-- 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 #

**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/controllers/hcloudmachine_controller.go b/controllers/hcloudmachine_controller.go
index c204f6eb..c1be0ad6 100644
--- a/controllers/hcloudmachine_controller.go
+++ b/controllers/hcloudmachine_controller.go
@@ -48,6 +48,7 @@ import (
 // HCloudMachineReconciler reconciles a HCloudMachine object.
 type HCloudMachineReconciler struct {
 	client.Client
+	RateLimitWaitTime   time.Duration
 	APIReader           client.Reader
 	HCloudClientFactory hcloudclient.Factory
 	WatchFilterValue    string
@@ -156,7 +157,7 @@ func (r *HCloudMachineReconciler) Reconcile(ctx context.Context, req reconcile.R
 	}()
 
 	// Check whether rate limit has been reached and if so, then wait.
-	if wait := reconcileRateLimit(hcloudMachine); wait {
+	if wait := reconcileRateLimit(hcloudMachine, r.RateLimitWaitTime); wait {
 		return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 	}
 
diff --git a/controllers/hcloudmachinetemplate_controller.go b/controllers/hcloudmachinetemplate_controller.go
index 2bbbd620..247f91ec 100644
--- a/controllers/hcloudmachinetemplate_controller.go
+++ b/controllers/hcloudmachinetemplate_controller.go
@@ -43,6 +43,7 @@ import (
 // HCloudMachineTemplateReconciler reconciles a HCloudMachineTemplate object.
 type HCloudMachineTemplateReconciler struct {
 	client.Client
+	RateLimitWaitTime   time.Duration
 	APIReader           client.Reader
 	HCloudClientFactory hcloudclient.Factory
 	WatchFilterValue    string
@@ -125,7 +126,7 @@ func (r *HCloudMachineTemplateReconciler) Reconcile(ctx context.Context, req rec
 	}()
 
 	// check whether rate limit has been reached and if so, then wait.
-	if wait := reconcileRateLimit(machineTemplate); wait {
+	if wait := reconcileRateLimit(machineTemplate, r.RateLimitWaitTime); wait {
 		return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 	}
 
diff --git a/controllers/hcloudremediation_controller.go b/controllers/hcloudremediation_controller.go
index 347dbb31..b41a8163 100644
--- a/controllers/hcloudremediation_controller.go
+++ b/controllers/hcloudremediation_controller.go
@@ -45,6 +45,7 @@ import (
 // HCloudRemediationReconciler reconciles a HCloudRemediation object.
 type HCloudRemediationReconciler struct {
 	client.Client
+	RateLimitWaitTime   time.Duration
 	APIReader           client.Reader
 	HCloudClientFactory hcloudclient.Factory
 	WatchFilterValue    string
@@ -170,7 +171,7 @@ func (r *HCloudRemediationReconciler) Reconcile(ctx context.Context, req reconci
 	}()
 
 	// Check whether rate limit has been reached and if so, then wait.
-	if wait := reconcileRateLimit(hcloudRemediation); wait {
+	if wait := reconcileRateLimit(hcloudRemediation, r.RateLimitWaitTime); wait {
 		return reconcile.Result{RequeueAfter: 30 * time.Second}, nil
 	}
 
diff --git a/controllers/hetznerbaremetalhost_controller.go b/controllers/hetznerbaremetalhost_controller.go
index 72a22514..d4ecee01 100644
--- a/controllers/hetznerbaremetalhost_controller.go
+++ b/controllers/hetznerbaremetalhost_controller.go
@@ -51,6 +51,7 @@ import (
 // HetznerBareMetalHostReconciler reconciles a HetznerBareMetalHost object.
 type HetznerBareMetalHostReconciler struct {
 	client.Client
+	RateLimitWaitTime  time.Duration
 	APIReader          client.Reader
 	RobotClientFactory robotclient.Factory
 	SSHClientFactory   sshclient.Factory
@@ -138,7 +139,7 @@ func (r *HetznerBareMetalHostReconciler) Reconcile(ctx context.Context, req ctrl
 	}
 
 	// check whether rate limit has been reached and if so, then wait.
-	if wait := reconcileRateLimit(bmHost); wait {
+	if wait := reconcileRateLimit(bmHost, r.RateLimitWaitTime); wait {
 		log.Info("Rate limit exceeded - requeue in 30 seconds")
 		return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
 	}
diff --git a/controllers/hetznercluster_controller.go b/controllers/hetznercluster_controller.go
index 94b11c65..9dea4187 100644
--- a/controllers/hetznercluster_controller.go
+++ b/controllers/hetznercluster_controller.go
@@ -62,12 +62,12 @@ import (
 
 const (
 	secretErrorRetryDelay = time.Second * 10
-	rateLimitWaitTime     = 5 * time.Minute
 )
 
 // HetznerClusterReconciler reconciles a HetznerCluster object.
 type HetznerClusterReconciler struct {
 	client.Client
+	RateLimitWaitTime              time.Duration
 	APIReader                      client.Reader
 	HCloudClientFactory            hcloudclient.Factory
 	targetClusterManagersStopCh    map[types.NamespacedName]chan struct{}
@@ -153,7 +153,7 @@ func (r *HetznerClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
 	}()
 
 	// check whether rate limit has been reached and if so, then wait.
-	if wait := reconcileRateLimit(hetznerCluster); wait {
+	if wait := reconcileRateLimit(hetznerCluster, r.RateLimitWaitTime); wait {
 		return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
 	}
 
@@ -335,7 +335,7 @@ func (r *HetznerClusterReconciler) reconcileDelete(ctx context.Context, clusterS
 
 // reconcileRateLimit checks whether a rate limit has been reached and returns whether
 // the controller should wait a bit more.
-func reconcileRateLimit(setter conditions.Setter) bool {
+func reconcileRateLimit(setter conditions.Setter, rateLimitWaitTime time.Duration) bool {
 	condition := conditions.Get(setter, infrav1.HetznerAPIReachableCondition)
 	if condition != nil && condition.Status == corev1.ConditionFalse {
 		if time.Now().Before(condition.LastTransitionTime.Time.Add(rateLimitWaitTime)) {
diff --git a/main.go b/main.go
index 8146dbdf..efbe2284 100644
--- a/main.go
+++ b/main.go
@@ -71,6 +71,7 @@ var (
 	hetznerBareMetalHostConcurrency    int
 	logLevel                           string
 	syncPeriod                         time.Duration
+	rateLimitWaitTime                  time.Duration
 )
 
 func main() {
@@ -85,7 +86,8 @@ func main() {
 	flag.IntVar(&hetznerBareMetalMachineConcurrency, "hetznerbaremetalmachine-concurrency", 1, "Number of HetznerBareMetalMachines to process simultaneously")
 	flag.IntVar(&hetznerBareMetalHostConcurrency, "hetznerbaremetalhost-concurrency", 1, "Number of HetznerBareMetalHosts to process simultaneously")
 	flag.StringVar(&logLevel, "log-level", "debug", "Specifies log level. Options are 'debug', 'info' and 'error'")
-	flag.DurationVar(&syncPeriod, "sync-period", time.Duration(3*time.Minute), "The minimum interval at which watched resources are reconciled (e.g. 3m)")
+	flag.DurationVar(&syncPeriod, "sync-period", 3*time.Minute, "The minimum interval at which watched resources are reconciled (e.g. 3m)")
+	flag.DurationVar(&rateLimitWaitTime, "rate-limit", 5*time.Minute, "The rate limiting for HCloud controller (e.g. 5m)")
 	flag.Parse()
 
 	ctrl.SetLogger(utils.GetDefaultLogger(logLevel))
@@ -125,6 +127,7 @@ func main() {
 	if err = (&controllers.HetznerClusterReconciler{
 		Client:                         mgr.GetClient(),
 		APIReader:                      mgr.GetAPIReader(),
+		RateLimitWaitTime:              rateLimitWaitTime,
 		HCloudClientFactory:            hcloudClientFactory,
 		WatchFilterValue:               watchFilterValue,
 		TargetClusterManagersWaitGroup: &wg,


Test patch:
diff --git a/controllers/controllers_suite_test.go b/controllers/controllers_suite_test.go
index 82f3f745..f7d84068 100644
--- a/controllers/controllers_suite_test.go
+++ b/controllers/controllers_suite_test.go
@@ -70,6 +70,7 @@ var _ = BeforeSuite(func() {
 	Expect((&HetznerClusterReconciler{
 		Client:                         testEnv.Manager.GetClient(),
 		APIReader:                      testEnv.Manager.GetAPIReader(),
+		RateLimitWaitTime:              5 * time.Minute,
 		HCloudClientFactory:            testEnv.HCloudClientFactory,
 		TargetClusterManagersWaitGroup: &wg,
 	}).SetupWithManager(ctx, testEnv.Manager, controller.Options{})).To(Succeed())
diff --git a/controllers/hetznercluster_controller_test.go b/controllers/hetznercluster_controller_test.go
index eb20eeca..29d52d4c 100644
--- a/controllers/hetznercluster_controller_test.go
+++ b/controllers/hetznercluster_controller_test.go
@@ -762,22 +762,22 @@ var _ = Describe("reconcileRateLimit", func() {
 
 	It("returns wait==true if rate limit exceeded is set and time is not over", func() {
 		conditions.MarkFalse(hetznerCluster, infrav1.HetznerAPIReachableCondition, infrav1.RateLimitExceededReason, clusterv1.ConditionSeverityWarning, "")
-		Expect(reconcileRateLimit(hetznerCluster)).To(BeTrue())
+		Expect(reconcileRateLimit(hetznerCluster, testEnv.RateLimitWaitTime)).To(BeTrue())
 	})
 
 	It("returns wait==false if rate limit exceeded is set and time is over", func() {
 		conditions.MarkFalse(hetznerCluster, infrav1.HetznerAPIReachableCondition, infrav1.RateLimitExceededReason, clusterv1.ConditionSeverityWarning, "")
 		conditionList := hetznerCluster.GetConditions()
 		conditionList[0].LastTransitionTime = metav1.NewTime(time.Now().Add(-time.Hour))
-		Expect(reconcileRateLimit(hetznerCluster)).To(BeFalse())
+		Expect(reconcileRateLimit(hetznerCluster, testEnv.RateLimitWaitTime)).To(BeFalse())
 	})
 
 	It("returns wait==false if rate limit condition is set to true", func() {
 		conditions.MarkTrue(hetznerCluster, infrav1.HetznerAPIReachableCondition)
-		Expect(reconcileRateLimit(hetznerCluster)).To(BeFalse())
+		Expect(reconcileRateLimit(hetznerCluster, testEnv.RateLimitWaitTime)).To(BeFalse())
 	})
 
 	It("returns wait==false if rate limit condition is not set", func() {
-		Expect(reconcileRateLimit(hetznerCluster)).To(BeFalse())
+		Expect(reconcileRateLimit(hetznerCluster, testEnv.RateLimitWaitTime)).To(BeFalse())
 	})
 })
diff --git a/test/helpers/envtest.go b/test/helpers/envtest.go
index a43db630..274db0d0 100644
--- a/test/helpers/envtest.go
+++ b/test/helpers/envtest.go
@@ -25,6 +25,7 @@ import (
 	"path"
 	"path/filepath"
 	goruntime "runtime"
+	"time"
 
 	g "github.com/onsi/ginkgo/v2"
 	infrav1 "github.com/syself/cluster-api-provider-hetzner/api/v1beta1"
@@ -114,6 +115,7 @@ type (
 		OSSSHClientAfterCloudInit    *sshmock.Client
 		RobotClient                  *robotmock.Client
 		cancel                       context.CancelFunc
+		RateLimitWaitTime            time.Duration
 	}
 )
 
@@ -194,6 +196,7 @@ func NewTestEnvironment() *TestEnvironment {
 		OSSSHClientAfterCloudInit:    osSSHClientAfterCloudInit,
 		RobotClientFactory:           mocks.NewRobotFactory(robotClient),
 		RobotClient:                  robotClient,
+		RateLimitWaitTime:            5 * time.Minute,
 	}
 }