Skip to content

Commit 1482225

Browse files
committed
add default implementations for RequeueConfiguration/RetryConfiguration
implementations may embed component.ReqeuueSpec and/or component.RetrySpec into their component's spec, in order to provide tuning the requeue and/or retry interval
1 parent b97ee65 commit 1482225

File tree

5 files changed

+204
-17
lines changed

5 files changed

+204
-17
lines changed

pkg/component/component.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package component
88
import (
99
"fmt"
1010
"reflect"
11+
"time"
1112

1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1314
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -97,8 +98,6 @@ func (s *PlacementSpec) GetDeploymentName() string {
9798
return s.Name
9899
}
99100

100-
var _ PlacementConfiguration = &PlacementSpec{}
101-
102101
// Implement the ClientConfiguration interface.
103102
func (s *ClientSpec) GetKubeConfig() []byte {
104103
if s.KubeConfig == nil {
@@ -107,8 +106,6 @@ func (s *ClientSpec) GetKubeConfig() []byte {
107106
return s.KubeConfig.SecretRef.value
108107
}
109108

110-
var _ ClientConfiguration = &ClientSpec{}
111-
112109
// Implement the ImpersonationConfiguration interface.
113110
func (s *ImpersonationSpec) GetImpersonationUser() string {
114111
if s.ServiceAccountName == "" {
@@ -124,7 +121,21 @@ func (s *ImpersonationSpec) GetImpersonationGroups() []string {
124121
return nil
125122
}
126123

127-
var _ ImpersonationConfiguration = &ImpersonationSpec{}
124+
// Implement the RequeueConfiguration interface.
125+
func (s *RequeueSpec) GetRequeueInterval() time.Duration {
126+
if s.RequeueInterval != nil {
127+
return s.RequeueInterval.Duration
128+
}
129+
return time.Duration(0)
130+
}
131+
132+
// Implement the RetryConfiguration interface.
133+
func (s *RetrySpec) GetRetryInterval() time.Duration {
134+
if s.RetryInterval != nil {
135+
return s.RetryInterval.Duration
136+
}
137+
return time.Duration(0)
138+
}
128139

129140
// Get state (and related details).
130141
func (s *Status) GetState() (State, string, string) {

pkg/component/reconcile.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const (
7878
// HookFunc is the function signature that can be used to
7979
// establish callbacks at certain points in the reconciliation logic.
8080
// Hooks will be passed the current (potentially unsaved) state of the component.
81-
// Post-hooks will only be called if the previous operator (read, reconcile, delete)
81+
// Post-hooks will only be called if the according operation (read, reconcile, delete)
8282
// has been successful.
8383
type HookFunc[T Component] func(ctx context.Context, client client.Client, component T) error
8484

@@ -114,6 +114,7 @@ func NewReconciler[T Component](name string, client client.Client, discoveryClie
114114

115115
// Reconcile contains the actual reconciliation logic.
116116
func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
117+
// TODO: add some check (and lock?) to prevent Reconcile() to be invoked before SetupWithManager() was called
117118
log := log.FromContext(ctx)
118119
log.V(1).Info("running reconcile")
119120

@@ -351,6 +352,7 @@ func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T] {
351352
}
352353

353354
// Register the reconciler with a given controller-runtime Manager.
355+
// TODO: probably we could merge NewReconciler() and SetupWithManager() into one function, such as SetupReconciler().
354356
func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error {
355357
// TODO: add some check (and lock?) to prevent SetupWithManager() being called more than once
356358
kubeSystemNamespace := &corev1.Namespace{}

pkg/component/types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,17 @@ type PlacementSpec struct {
9191
Name string `json:"name,omitempty"`
9292
}
9393

94+
var _ PlacementConfiguration = &PlacementSpec{}
95+
9496
// +kubebuilder:object:generate=true
9597

9698
// ClientSpec defines a reference to another cluster by kubeconfig. Components providing ClientConfiguration may include this into their spec.
9799
type ClientSpec struct {
98100
KubeConfig *KubeConfigSpec `json:"kubeConfig,omitempty"`
99101
}
100102

103+
var _ ClientConfiguration = &ClientSpec{}
104+
101105
// +kubebuilder:object:generate=true
102106

103107
// KubeConfigSpec defines a reference to a kubeconfig.
@@ -112,6 +116,32 @@ type ImpersonationSpec struct {
112116
ServiceAccountName string `json:"serviceAccountName,omitempty"`
113117
}
114118

119+
var _ ImpersonationConfiguration = &ImpersonationSpec{}
120+
121+
// +kubebuilder:object:generate=true
122+
123+
// RequeueSpec defines the requeue interval, that is, the interval after which components will be re-reconciled after a successful reconciliation.
124+
// Components providing RequeueConfiguration may include this into their spec.
125+
type RequeueSpec struct {
126+
// +kubebuilder:validation:Type:=string
127+
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$"
128+
RequeueInterval *metav1.Duration `json:"requeueInterval,omitempty"`
129+
}
130+
131+
var _ RequeueConfiguration = &RequeueSpec{}
132+
133+
// +kubebuilder:object:generate=true
134+
135+
// RetrySpec defines the retry interval, that is, the interval after which components will be re-reconciled after a successful reconciliation.
136+
// Components providing RetryConfiguration may include this into their spec.
137+
type RetrySpec struct {
138+
// +kubebuilder:validation:Type:=string
139+
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$"
140+
RetryInterval *metav1.Duration `json:"retryInterval,omitempty"`
141+
}
142+
143+
var _ RetryConfiguration = &RetrySpec{}
144+
115145
// +kubebuilder:object:generate=true
116146

117147
// Component Status. Components must include this into their status.

pkg/component/zz_generated.deepcopy.go

Lines changed: 48 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/content/en/docs/concepts/reconciler.md

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ description: >
99

1010
Dependent objects are - by definition - the resources returned by the `Generate()` method of the used resource generator.
1111
Whenever a component resource (that is, an instance of the component's custom resource type) is created, udpated, or deleted,
12-
the set of dependent object probably changes, and the cluster state has to be synchronized with that new declared state.
12+
the set of dependent object potentially changes, and the cluster state has to be synchronized with that new declared state.
13+
This synchronization is the job of the reconciler provided by this framework.
1314

14-
This is the job of the reconciler which is instantiated by calling the following constructor:
15+
## Creating the reconciler instance
16+
17+
Typically, a component operator runs one reconciler which is instantiated by calling the following constructor:
1518

1619
```go
1720
package component
@@ -28,13 +31,15 @@ func NewReconciler[T Component](
2831

2932
The passed type parameter `T Component` is the concrete runtime type of the component's custom resource type. Furthermore,
3033
- `name` is supposed to be a unique name (typically a DNS name) identifying this component operator in the cluster; ìt will be used in annotations, labels, for leader election, ...
31-
- `client`, `discoveryClient`, `eventRecorder` and `scheme` are deprecated and will be removed in future releases; they can be passed as `nil`
34+
- `client`, `discoveryClient`, `eventRecorder` and `scheme` are ignored and will be removed in future releases; they can be passed as `nil`
3235
- `resourceGenerator` is an implementation of the `Generator` interface, describing how the dependent objects are rendered from the component's spec.
3336

3437
The object returned by `NewReconciler` implements controller-runtime's `Reconciler` interface, and can therefore be used as a drop-in
3538
in kubebuilder managed projects. After creation, the reconciler has to be registered with the responsible controller-runtime manager instance by calling
3639

3740
```go
41+
package component
42+
3843
func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error
3944
```
4045

@@ -47,4 +52,102 @@ The used manager `mgr` has to fulfill a few requirements:
4752
- the types in the API group defined in this repository
4853
- the core group (`v1`)
4954
- group `apiextensions.k8s.io/v1`
50-
- group `apiregistration.k8s.io/v1`.
55+
- group `apiregistration.k8s.io/v1`.
56+
57+
## Reconciler hooks
58+
59+
Component operators may register hooks to enhance the reconciler logic at certain points, by passing functions of type
60+
61+
```go
62+
package component
63+
64+
// HookFunc is the function signature that can be used to
65+
// establish callbacks at certain points in the reconciliation logic.
66+
// Hooks will be passed the current (potentially unsaved) state of the component.
67+
// Post-hooks will only be called if the according operation (read, reconcile, delete)
68+
// has been successful.
69+
type HookFunc[T Component] func(ctx context.Context, client client.Client, component T) error
70+
```
71+
72+
to the desired registration functions:
73+
74+
```go
75+
package component
76+
77+
// Register post-read hook with reconciler.
78+
// This hook will be called after the reconciled component object has been retrieved from the Kubernetes API.
79+
func (r *Reconciler[T]) WithPostReadHook(hook HookFunc[T]) *Reconciler[T]
80+
81+
// Register pre-reconcile hook with reconciler.
82+
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
83+
// right before the reconcilation of the dependent objects starts.
84+
func (r *Reconciler[T]) WithPreReconcileHook(hook HookFunc[T]) *Reconciler[T]
85+
86+
// Register post-reconcile hook with reconciler.
87+
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
88+
// right after the reconcilation of the dependent objects happened, and was successful.
89+
func (r *Reconciler[T]) WithPostReconcileHook(hook HookFunc[T]) *Reconciler[T]
90+
91+
// Register pre-delete hook with reconciler.
92+
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
93+
// right before the deletion of the dependent objects starts.
94+
func (r *Reconciler[T]) WithPreDeleteHook(hook HookFunc[T]) *Reconciler[T]
95+
96+
// Register post-delete hook with reconciler.
97+
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
98+
// right after the deletion of the dependent objects happened, and was successful.
99+
func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T]
100+
```
101+
102+
Note that the client passed to the hook functions is the client of the manager that was used when calling `SetupWithManager()`
103+
(that is, the return value of that manager's `GetClient()` method).
104+
105+
## Tuning the retry behavior
106+
107+
By default, errors returned by the component's generator or by a registered hook will make the reconciler go
108+
into a backoff managed by controller-runtime (which usually is an exponential backoff, capped at 10 minutes).
109+
However, if the error is or unwraps to a `types.RetriableError`, then the retry delay specified at the error
110+
will be used instead of the backoff. Implementations should use
111+
112+
```go
113+
pacakge types
114+
115+
func NewRetriableError(err error, retryAfter *time.Duration) RetriableError {
116+
return RetriableError{err: err, retryAfter: retryAfter}
117+
}
118+
```
119+
120+
to wrap an error into a `RetriableError`. It is allowed to pass `retryAfter` as nil; in that case the retry delay
121+
will be determined by calling the component's `GetRetryInterval()` method (if the component or its spec implements
122+
the
123+
124+
```go
125+
package component
126+
127+
// The RetryConfiguration interface is meant to be implemented by components (or their spec) which offer
128+
// tweaking the retry interval (by default, it would be the value of the requeue interval).
129+
type RetryConfiguration interface {
130+
// Get retry interval. Should be greater than 1 minute.
131+
GetRetryInterval() time.Duration
132+
}
133+
```
134+
135+
interface), or otherwise will be set to the effective requeue interval (see below).
136+
137+
## Tuning the requeue behavior
138+
139+
If a component was successfully reconciled, another reconciliation will be scheduled after 10 minutes, by default.
140+
This default requeue interval may be overridden by the component by implementing the
141+
142+
```go
143+
package component
144+
145+
// The RequeueConfiguration interface is meant to be implemented by components (or their spec) which offer
146+
// tweaking the requeue interval (by default, it would be 10 minutes).
147+
type RequeueConfiguration interface {
148+
// Get requeue interval. Should be greater than 1 minute.
149+
GetRequeueInterval() time.Duration
150+
}
151+
```
152+
153+
interface.

0 commit comments

Comments
 (0)