Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/libs/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ You can then `Build()` the status updater and run `UpdateStatus()` to do the act
- The `AllStatusFields()` function returns a list containing all status field keys, _except the one for the status field itself_, for convenience.
- The `WithCustomUpdateFunc` method can be used to inject a function that performs custom logic on the resource's status. Note that while the function gets the complete object as an argument, only changes to its status will be updated by the status updater.
- `WithConditionEvents` can be used to enable event recording for changed conditions. The events are automatically connected to the resource from the `ReconcileResult`'s `Object` field, no events will be recorded if that field is `nil`.
- By using `WithSmartRequeue`, the [smart requeuing logic](./smartrequeue.md) can be used.
- A `smartrequeue.Store` is required to be configured outside of the status updater, because it has to be persisted across multiple reconciliations.
- It is also possible to use the smart requeue logic explicitly and modify the `ReconcileResult`'s `Result` field with the returned value, but the integration should be easier to use, since both, the smart requeue logic as well as the status updater, return a `reconcile.Result` and an `error`, which are intended to be directly used as return values for the `Reconcile` method.

### The ReconcileResult

Expand Down
47 changes: 47 additions & 0 deletions pkg/controller/status_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/openmcp-project/controller-utils/pkg/conditions"
"github.com/openmcp-project/controller-utils/pkg/controller/smartrequeue"
"github.com/openmcp-project/controller-utils/pkg/errors"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -120,6 +121,29 @@ func (b *StatusUpdaterBuilder[Obj]) WithCustomUpdateFunc(f func(obj Obj, rr Reco
return b
}

type SmartRequeueAction string

const (
SR_BACKOFF SmartRequeueAction = "Backoff"
SR_RESET SmartRequeueAction = "Reset"
SR_NO_REQUEUE SmartRequeueAction = "NoRequeue"
)

// WithSmartRequeue integrates the smart requeue logic into the status updater.
// Requires a smartrequeue.Store to be passed in (this needs to be persistent across multiple reconciliations and therefore cannot be stored in the status updater itself).
// The action determines when the object should be requeued:
// - "Backoff": the object is requeued with an increasing backoff, as specified in the store.
// - "Reset": the object is requeued, but the backoff is reset to its minimal value, as specified in the store.
// - "NoRequeue": the object is not requeued.
// If the 'Result' field in the ReconcileResult has a non-zero RequeueAfter value set, that one is used if it is earlier than the one from smart requeue or if "NoRequeue" has been specified.
// This function only has an effect if the Object in the ReconcileResult is not nil, the smart requeue store is not nil, and the action is one of the known values.
// Also, if a reconciliation error occurred, the requeue interval will be reset, but no requeueAfter duration will be set, because controller-runtime will take care of requeuing the object anyway.
func (b *StatusUpdaterBuilder[Obj]) WithSmartRequeue(store *smartrequeue.Store, action SmartRequeueAction) *StatusUpdaterBuilder[Obj] {
b.internal.smartRequeueStore = store
b.internal.smartRequeueAction = action
return b
}

// Build returns the status updater.
func (b *StatusUpdaterBuilder[Obj]) Build() *statusUpdater[Obj] {
return b.internal
Expand Down Expand Up @@ -158,6 +182,8 @@ type statusUpdater[Obj client.Object] struct {
removeUntouchedConditions bool
eventRecorder record.EventRecorder
eventVerbosity conditions.EventVerbosity
smartRequeueStore *smartrequeue.Store
smartRequeueAction SmartRequeueAction
}

func newStatusUpdater[Obj client.Object]() *statusUpdater[Obj] {
Expand All @@ -184,6 +210,8 @@ func defaultPhaseUpdateFunc[Obj client.Object](obj Obj, _ ReconcileResult[Obj])
// UpdateStatus updates the status of the object in the given ReconcileResult, using the previously set field names and functions.
// The object is expected to be a pointer to a struct with the status field.
// If the 'Object' field in the ReconcileResult is nil, the status update becomes a no-op.
//
//nolint:gocyclo
func (s *statusUpdater[Obj]) UpdateStatus(ctx context.Context, c client.Client, rr ReconcileResult[Obj]) (ctrl.Result, error) {
errs := errors.NewReasonableErrorList(rr.ReconcileError)
if IsNil(rr.Object) {
Expand Down Expand Up @@ -259,6 +287,25 @@ func (s *statusUpdater[Obj]) UpdateStatus(ctx context.Context, c client.Client,
errs.Append(fmt.Errorf("error patching status: %w", err))
}

if s.smartRequeueStore != nil {
var srRes ctrl.Result
if rr.ReconcileError != nil {
srRes, _ = s.smartRequeueStore.For(rr.Object).Error(rr.ReconcileError)
} else {
switch s.smartRequeueAction {
case SR_BACKOFF:
srRes, _ = s.smartRequeueStore.For(rr.Object).Backoff()
case SR_RESET:
srRes, _ = s.smartRequeueStore.For(rr.Object).Reset()
case SR_NO_REQUEUE:
srRes, _ = s.smartRequeueStore.For(rr.Object).Never()
}
}
if srRes.RequeueAfter > 0 && (rr.Result.RequeueAfter == 0 || srRes.RequeueAfter < rr.Result.RequeueAfter) {
rr.Result.RequeueAfter = srRes.RequeueAfter
}
}

return rr.Result, errs.Aggregate()
}

Expand Down
52 changes: 52 additions & 0 deletions pkg/controller/status_updater_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import (
. "github.com/onsi/gomega"

"github.com/openmcp-project/controller-utils/pkg/conditions"
"github.com/openmcp-project/controller-utils/pkg/controller/smartrequeue"
. "github.com/openmcp-project/controller-utils/pkg/testing/matchers"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/scheme"

Expand Down Expand Up @@ -194,6 +196,56 @@ var _ = Describe("Status Updater", func() {
}
})

Context("Smart Requeue", func() {

It("should add a requeueAfter duration if configured", func() {
env := testutils.NewEnvironmentBuilder().WithFakeClient(coScheme).WithInitObjectPath("testdata", "test-02").WithDynamicObjectsWithStatus(&CustomObject{}).Build()
obj := &CustomObject{}
Expect(env.Client().Get(env.Ctx, controller.ObjectKey("status", "default"), obj)).To(Succeed())
rr := controller.ReconcileResult[*CustomObject]{
Object: obj,
Conditions: dummyConditions(),
}
store := smartrequeue.NewStore(1*time.Second, 10*time.Second, 2.0)
su := preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
return PhaseSucceeded, nil
}).WithSmartRequeue(store, controller.SR_RESET).Build()
res, err := su.UpdateStatus(env.Ctx, env.Client(), rr)
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(1 * time.Second))
})

It("should keep the smaller requeueAfter duration if smart requeue and RequeueAfter in the ReconcileResult are set", func() {
env := testutils.NewEnvironmentBuilder().WithFakeClient(coScheme).WithInitObjectPath("testdata", "test-02").WithDynamicObjectsWithStatus(&CustomObject{}).Build()
obj := &CustomObject{}
Expect(env.Client().Get(env.Ctx, controller.ObjectKey("status", "default"), obj)).To(Succeed())
rr := controller.ReconcileResult[*CustomObject]{
Object: obj,
Conditions: dummyConditions(),
Result: ctrl.Result{
RequeueAfter: 30 * time.Second,
},
}
store := smartrequeue.NewStore(1*time.Second, 10*time.Second, 2.0)
su := preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
return PhaseSucceeded, nil
}).WithSmartRequeue(store, controller.SR_RESET).Build()
res, err := su.UpdateStatus(env.Ctx, env.Client(), rr)
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(1 * time.Second))

rr.Result.RequeueAfter = 30 * time.Second
store = smartrequeue.NewStore(1*time.Minute, 10*time.Minute, 2.0)
su = preconfiguredStatusUpdaterBuilder().WithPhaseUpdateFunc(func(obj *CustomObject, rr controller.ReconcileResult[*CustomObject]) (string, error) {
return PhaseSucceeded, nil
}).WithSmartRequeue(store, controller.SR_RESET).Build()
res, err = su.UpdateStatus(env.Ctx, env.Client(), rr)
Expect(err).ToNot(HaveOccurred())
Expect(res.RequeueAfter).To(Equal(30 * time.Second))
})

})

Context("GenerateCreateConditionFunc", func() {

It("should add the condition to the given ReconcileResult", func() {
Expand Down