Skip to content

Commit a778a14

Browse files
santiago-venturashilparamasamyreddyRalfHammer
authored
Increase or Decrease polling rate for instance in a final state ready or failed. (#53)
* Initial commit for decrease polling interval for instances in a final state * Add reconciler helpers * Refactoring helper function, and adding unit tests * add second annotation for fail * Update official documentation. Adding a new page called, Annotations. * Add license header missed in new Go files. React to reviewer comment about the service binding controller. * React to reviewer comment in PR. Removing old comments. * Update website/content/en/docs/tutorials/annotations.md Co-authored-by: RalfHammer <[email protected]> * Update reconciler_helpers.go * Change annotations.md following a reviewer comment. --------- Co-authored-by: shilparamasamyreddy <[email protected]> Co-authored-by: RalfHammer <[email protected]>
1 parent 501162d commit a778a14

File tree

7 files changed

+243
-46
lines changed

7 files changed

+243
-46
lines changed

api/v1alpha1/constants.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const (
1717
AnnotationMaxRetries = "service-operator.cf.cs.sap.com/max-retries"
1818
// annotation to hold the reconciliation timeout value
1919
AnnotationReconcileTimeout = "service-operator.cf.cs.sap.com/timeout-on-reconcile"
20+
// annotation to increase or decrease the requeue interval at which the operator polls the status of CR after final state ready.
21+
AnnotationPollingIntervalReady = "service-operator.cf.cs.sap.com/polling-interval-ready"
22+
// annotation to increase or decrease the requeue interval at which the operator polls the status of CR after final state failed.
23+
AnnotationPollingIntervalFail = "service-operator.cf.cs.sap.com/polling-interval-fail"
2024
// annotation to adopt orphan CF resources. If set to 'adopt', the operator will adopt orphan CF resource.
2125
// Ex. "service-operator.cf.cs.sap.com/adopt-cf-resources"="adopt"
2226
AnnotationAdoptCFResources = "service-operator.cf.cs.sap.com/adopt-cf-resources"
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/*
7+
Package controllers contains the implementation of various helper functions used by the reconciler.
8+
*/
9+
10+
package controllers
11+
12+
import (
13+
"strconv"
14+
"time"
15+
16+
"github.com/go-logr/logr"
17+
cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1"
18+
ctrl "sigs.k8s.io/controller-runtime"
19+
)
20+
21+
// setMaxRetries sets the maximum number of retries for a service instance based on the value provided in the annotations
22+
// or uses the default value if the annotation is not set or is invalid.
23+
// TODO: Make it Generic so applies to Space and ServiceBinding.
24+
// TODO: Add a test for this function.
25+
func setMaxRetries(serviceInstance *cfv1alpha1.ServiceInstance, log logr.Logger) {
26+
// Default to an infinite number of retries
27+
serviceInstance.Status.MaxRetries = serviceInstanceDefaultMaxRetries
28+
29+
// Use max retries from annotation
30+
maxRetriesStr, found := serviceInstance.GetAnnotations()[cfv1alpha1.AnnotationMaxRetries]
31+
if found {
32+
maxRetries, err := strconv.Atoi(maxRetriesStr)
33+
if err != nil {
34+
log.V(1).Info("Invalid max retries annotation value, using default", "AnnotationMaxRetries", maxRetriesStr)
35+
} else {
36+
serviceInstance.Status.MaxRetries = maxRetries
37+
}
38+
}
39+
}
40+
41+
// getReconcileTimeout reads the reconcile timeout from the annotation on the service instance
42+
// or - if the annotation is not set - uses the default value serviceInstanceDefaultRequeueTimeout
43+
// or else returns the reconcile timeout as a time duration
44+
// TODO: Make it Generic so applies to Space and ServiceBinding.
45+
// TODO: Add a test for this function.
46+
func getReconcileTimeout(serviceInstance *cfv1alpha1.ServiceInstance) time.Duration {
47+
// Use reconcile timeout from annotation, use default if annotation is missing or not parsable
48+
reconcileTimeoutStr, ok := serviceInstance.GetAnnotations()[cfv1alpha1.AnnotationReconcileTimeout]
49+
if !ok {
50+
return serviceInstanceDefaultReconcileInterval
51+
}
52+
reconcileTimeout, err := time.ParseDuration(reconcileTimeoutStr)
53+
if err != nil {
54+
return serviceInstanceDefaultReconcileInterval
55+
}
56+
return reconcileTimeout
57+
}
58+
59+
// getPollingInterval retrieves the polling interval from the annotaion on the service instance
60+
// or - in case the annotation is not set or invalid - returns either the defaultDurationStr or an empty ctrl.Result{}.
61+
// Otherwise, it returns a ctrl.Result with the RequeueAfter field set in the annotation.
62+
func getPollingInterval(annotations map[string]string, defaultDurationStr, annotationName string) ctrl.Result {
63+
pollingIntervalStr, ok := annotations[annotationName]
64+
if ok {
65+
pollingInterval, err := time.ParseDuration(pollingIntervalStr)
66+
if err == nil {
67+
return ctrl.Result{RequeueAfter: pollingInterval}
68+
}
69+
}
70+
71+
// If the polling interval is not set, return the default duration
72+
defaultDuration, err := time.ParseDuration(defaultDurationStr)
73+
if err != nil {
74+
// If the default duration is not parsable, return an empty result
75+
return ctrl.Result{}
76+
}
77+
78+
return ctrl.Result{RequeueAfter: defaultDuration}
79+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package controllers
7+
8+
import (
9+
"testing"
10+
"time"
11+
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
14+
. "github.com/onsi/ginkgo/v2"
15+
. "github.com/onsi/gomega"
16+
cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1"
17+
ctrl "sigs.k8s.io/controller-runtime"
18+
)
19+
20+
func TestReconcilerHelpers(t *testing.T) {
21+
RegisterFailHandler(Fail)
22+
// RunSpecs(t, "Reconciler Helpers Suite")
23+
}
24+
25+
var _ = Describe("Create a instance with the polling interval annotation | GetPollingInterval", func() {
26+
27+
It("It should return a RequeueAfter of 10 Seconds time duration", func() {
28+
serviceInstance := &cfv1alpha1.ServiceInstance{
29+
ObjectMeta: metav1.ObjectMeta{
30+
Annotations: map[string]string{
31+
cfv1alpha1.AnnotationPollingIntervalReady: "10s",
32+
},
33+
},
34+
}
35+
36+
result := getPollingInterval(serviceInstance.GetAnnotations(), "100m", cfv1alpha1.AnnotationPollingIntervalReady)
37+
Expect(result.RequeueAfter).To(Equal(10 * time.Second))
38+
})
39+
40+
It("It should return a RequeueAfter of 2 Minutes time duration", func() {
41+
serviceInstance := &cfv1alpha1.ServiceInstance{
42+
ObjectMeta: metav1.ObjectMeta{
43+
Annotations: map[string]string{
44+
cfv1alpha1.AnnotationPollingIntervalFail: "2m",
45+
},
46+
},
47+
}
48+
49+
result := getPollingInterval(serviceInstance.GetAnnotations(), "100m", cfv1alpha1.AnnotationPollingIntervalFail)
50+
Expect(result.RequeueAfter).To(Equal(2 * time.Minute))
51+
})
52+
})
53+
54+
var _ = Describe("Create a ServiceBinding without the polling interval annotation | GetPollingInterval", func() {
55+
It("Should return a ctrl.Result with RequeueAfter of default duration", func() {
56+
defaultDurationStr := "100m"
57+
58+
serviceInstance := &cfv1alpha1.ServiceBinding{
59+
ObjectMeta: metav1.ObjectMeta{
60+
Annotations: map[string]string{},
61+
},
62+
}
63+
64+
result := getPollingInterval(serviceInstance.GetAnnotations(), defaultDurationStr, cfv1alpha1.AnnotationPollingIntervalReady)
65+
Expect(result).To(Equal(ctrl.Result{RequeueAfter: 100 * time.Minute}))
66+
})
67+
})
68+
69+
var _ = Describe("Create a Space instance with an invalid polling interval annotation | GetPollingInterval", func() {
70+
It("Should return a ctrl.Result with RequeueAfter of default duration", func() {
71+
defaultDurationStr := "10h"
72+
73+
serviceInstance := &cfv1alpha1.Space{
74+
ObjectMeta: metav1.ObjectMeta{
75+
Annotations: map[string]string{
76+
cfv1alpha1.AnnotationPollingIntervalReady: "invalid",
77+
},
78+
},
79+
}
80+
81+
result := getPollingInterval(serviceInstance.GetAnnotations(), defaultDurationStr, cfv1alpha1.AnnotationPollingIntervalReady)
82+
Expect(result).To(Equal(ctrl.Result{RequeueAfter: 10 * time.Hour}))
83+
})
84+
})
85+
86+
var _ = Describe("Create a Space instance without annotations and empty defaul time duration| GetPollingInterval", func() {
87+
It("Should return an empty ctrl.Result", func() {
88+
defaultDurationStr := ""
89+
90+
space := &cfv1alpha1.Space{
91+
ObjectMeta: metav1.ObjectMeta{},
92+
}
93+
94+
result := getPollingInterval(space.GetAnnotations(), defaultDurationStr, cfv1alpha1.AnnotationPollingIntervalReady)
95+
Expect(result).To(Equal(ctrl.Result{}))
96+
})
97+
})

internal/controllers/servicebinding_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque
355355
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil
356356
}
357357
// TODO: apply some increasing period, depending on the age of the last update
358-
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil
358+
return getPollingInterval(serviceBinding.GetAnnotations(), "10m", cfv1alpha1.AnnotationPollingIntervalReady), nil
359359
case facade.BindingStateCreatedFailed, facade.BindingStateDeleteFailed:
360360
serviceBinding.SetReadyCondition(cfv1alpha1.ConditionFalse, string(cfbinding.State), cfbinding.StateDescription)
361361
// TODO: apply some increasing period, depending on the age of the last update

internal/controllers/serviceinstance_controller.go

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"context"
1010
"fmt"
1111
"math"
12-
"strconv"
1312
"time"
1413

1514
"github.com/go-logr/logr"
@@ -42,7 +41,6 @@ const (
4241

4342
// Default values while waiting for ServiceInstance creation (state Progressing)
4443
serviceInstanceDefaultReconcileInterval = 1 * time.Second
45-
//serviceInstanceDefaultMaxReconcileInterval = 10 * time.Minute
4644

4745
// Default values for error cases during ServiceInstance creation
4846
serviceInstanceDefaultMaxRetries = math.MaxInt32 // infinite number of retries
@@ -111,7 +109,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ
111109
// Set a first status (and requeue, because the status update itself will not trigger another reconciliation because of the event filter set)
112110
if ready := serviceInstance.GetReadyCondition(); ready == nil {
113111
serviceInstance.SetReadyCondition(cfv1alpha1.ConditionUnknown, serviceInstanceReadyConditionReasonNew, "First seen")
114-
SetMaxRetries(serviceInstance, log)
112+
setMaxRetries(serviceInstance, log)
115113
return ctrl.Result{Requeue: true}, nil
116114
}
117115

@@ -385,8 +383,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ
385383
case facade.InstanceStateReady:
386384
serviceInstance.SetReadyCondition(cfv1alpha1.ConditionTrue, string(cfinstance.State), cfinstance.StateDescription)
387385
serviceInstance.Status.RetryCounter = 0 // Reset the retry counter
388-
// TODO: apply some increasing period, depending on the age of the last update
389-
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil
386+
return getPollingInterval(serviceInstance.GetAnnotations(), "10m",cfv1alpha1.AnnotationPollingIntervalReady), nil
390387
case facade.InstanceStateCreatedFailed, facade.InstanceStateUpdateFailed, facade.InstanceStateDeleteFailed:
391388
// Check if the retry counter exceeds the maximum allowed retries.
392389
// Check if the maximum retry limit is exceeded.
@@ -444,43 +441,6 @@ func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {
444441
Complete(r)
445442
}
446443

447-
// IncrementRetryCounterAndCheckRetryLimit increments the retry counter for a ServiceInstance and checks if the number of retries has exceeded the maximum allowed retries.
448-
// The maximum retries is configured per ServiceInstance via the annotation, AnnotationMaxRetries. If not specified,
449-
// a default value is used.
450-
// This function updates the ServiceInstance's Condition and State to indicate a failure when the retry limit is reached.
451-
// Returns:A boolean indicating whether the retry limit has been reached.
452-
func SetMaxRetries(serviceInstance *cfv1alpha1.ServiceInstance, log logr.Logger) {
453-
// Default to an infinite number number of retries
454-
serviceInstance.Status.MaxRetries = serviceInstanceDefaultMaxRetries
455-
456-
// Use max retries from annotation
457-
maxRetriesStr, found := serviceInstance.GetAnnotations()[cfv1alpha1.AnnotationMaxRetries]
458-
if found {
459-
maxRetries, err := strconv.Atoi(maxRetriesStr)
460-
if err != nil {
461-
log.V(1).Info("Invalid max retries annotation value, using default", "AnnotationMaxRetries", maxRetriesStr)
462-
} else {
463-
serviceInstance.Status.MaxRetries = maxRetries
464-
}
465-
}
466-
}
467-
468-
// function to read/get reconcile timeout annotation from the service instance "AnnotationReconcileTimeout = "service-operator.cf.cs.sap.com/timeout-on-reconcile" "
469-
// if the annotation is not set, the default value is used serviceInstanceDefaultRequeueTimeout
470-
// else returns the reconcile timeout as a time duration
471-
func getReconcileTimeout(serviceInstance *cfv1alpha1.ServiceInstance) time.Duration {
472-
// Use reconcile timeout from annotation, use default if annotation is missing or not parsable
473-
reconcileTimeoutStr, ok := serviceInstance.GetAnnotations()[cfv1alpha1.AnnotationReconcileTimeout]
474-
if !ok {
475-
return serviceInstanceDefaultReconcileInterval
476-
}
477-
reconcileTimeout, err := time.ParseDuration(reconcileTimeoutStr)
478-
if err != nil {
479-
return serviceInstanceDefaultReconcileInterval
480-
}
481-
return reconcileTimeout
482-
}
483-
484444
// HandleError sets conditions and the context to handle the error.
485445
// Special handling for retryable errros:
486446
// - retry after certain time interval
@@ -499,9 +459,8 @@ func (r *ServiceInstanceReconciler) HandleError(ctx context.Context, serviceInst
499459
if serviceInstance.Status.MaxRetries != serviceInstanceDefaultMaxRetries && serviceInstance.Status.RetryCounter >= serviceInstance.Status.MaxRetries {
500460
// Update the instance's status to reflect the failure due to too many retries.
501461
serviceInstance.SetReadyCondition(cfv1alpha1.ConditionFalse, "MaximumRetriesExceeded", "The service instance has failed due to too many retries.")
502-
return ctrl.Result{}, nil // finish reconcile loop
462+
return getPollingInterval(serviceInstance.GetAnnotations(), "",cfv1alpha1.AnnotationPollingIntervalFail), nil // finish reconcile loop
503463
}
504-
505464
// double the requeue interval
506465
condition := serviceInstance.GetReadyCondition()
507466
requeueAfter := 1 * time.Second

internal/controllers/space_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ func (r *SpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu
240240

241241
log.V(1).Info("healthcheck successful")
242242
space.SetReadyCondition(cfv1alpha1.ConditionTrue, spaceReadyConditionReasonSuccess, "Success")
243-
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil
243+
return getPollingInterval(space.GetAnnotations(), "60s",cfv1alpha1.AnnotationPollingIntervalReady), nil
244244
} else if len(serviceInstanceList.Items) > 0 {
245245
space.SetReadyCondition(cfv1alpha1.ConditionUnknown, spaceReadyConditionReasonDeletionBlocked, "Waiting for deletion of depending service instances")
246246
// TODO: apply some increasing period, depending on the age of the last update
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: "Annotations"
3+
linkTitle: "Annotations"
4+
weight: 20
5+
type: "docs"
6+
description: >
7+
How to control and optimize the CF Service Operator behavior via annotations.
8+
---
9+
10+
## Annotation Polling Interval Ready
11+
12+
The AnnotationPollingIntervalReady annotation is used to specify the duration of the requeue after interval at which the operator polls the status of a Custom Resource after final state ready. It is possible to apply this annotations to Space, ServiceInstance and ServiceBiding CRs.
13+
14+
By using this annotation, the code allows for flexible configuration of the polling interval, making it easier to adjust the readiness checking frequency based on specific requirements or conditions.
15+
16+
The value of the annotation is a string representing a duration, such as "100m" or "5h".
17+
18+
Usage:
19+
20+
```yaml
21+
apiVersion: cf.cs.sap.com/v1alpha1
22+
kind: ServiceInstance
23+
metadata:
24+
annotations:
25+
service-operator.cf.cs.sap.com/polling-interval-ready: "3h"
26+
```
27+
28+
In the example above the custom resource will be reconcile every three hours after reaching the state Ready.
29+
30+
**Default Requeue After Interval**
31+
32+
If the annotation AnnotationPollingIntervalReady is not set, the interval duration will be set to 10 minutes by default.
33+
34+
### Annotation Polling Interval Fail
35+
36+
The AnnotationPollingIntervalFail annotation is used to specify the duration of the requeue interval at which the operator polls the status of a Custom Resource after the final states Creation Failed and Deletion Failed. Currently it is possible to apply this annotations to ServiceInstance custom resource only.
37+
38+
By using this annotation, the code allows for flexible configuration of the polling interval, making it easier to adjust the re-queue frequency after the failure based on specific requirements or conditions.
39+
40+
The value of the annotation is a string representing a duration, such as "20s" or "10m".
41+
42+
Usage:
43+
44+
```yaml
45+
apiVersion: cf.cs.sap.com/v1alpha1
46+
kind: ServiceInstance
47+
metadata:
48+
annotations:
49+
service-operator.cf.cs.sap.com/polling-interval-fail: "5m"
50+
```
51+
52+
In the example above the custom resource will be reconcile every five minutes after reaching the final state Failed.
53+
54+
**Default Requeue After Interval**
55+
56+
If the annotation AnnotationPollingIntervalFail is not set, there won't be an immediate requeue. This means the resource will not be re-reconciled right away. The operator will consider the custom resource to be in a stable state, at least for now.
57+
58+
That means there is no default time duration for it, and it will return an empty result, ctrl.Result{}.

0 commit comments

Comments
 (0)