Skip to content

Commit d8131ae

Browse files
authored
make reapply interval configurable (#289)
1 parent 2bab7f8 commit d8131ae

File tree

8 files changed

+159
-9
lines changed

8 files changed

+159
-9
lines changed

pkg/component/component.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ func getSpec[T Component](component T) any {
3030
return spec.Interface()
3131
}
3232

33+
/*
34+
// TODO: should we use this instead of the typed assert functions?
35+
// Check if given component or its spec implements the specified Configuration type (and return it).
36+
func assertConfiguration[T Component, C any](component T) (C, bool) {
37+
if configuration, ok := Component(component).(C); ok {
38+
return configuration, true
39+
}
40+
if configuration, ok := getSpec(component).(C); ok {
41+
return configuration, true
42+
}
43+
return *new(C), false
44+
}
45+
*/
46+
3347
// Check if given component or its spec implements PlacementConfiguration (and return it).
3448
func assertPlacementConfiguration[T Component](component T) (PlacementConfiguration, bool) {
3549
if placementConfiguration, ok := Component(component).(PlacementConfiguration); ok {
@@ -118,6 +132,17 @@ func assertTypeConfiguration[T Component](component T) (TypeConfiguration, bool)
118132
return nil, false
119133
}
120134

135+
// Check if given component or its spec implements ReapplyConfiguration (and return it).
136+
func assertReapplyConfiguration[T Component](component T) (ReapplyConfiguration, bool) {
137+
if reapplyConfiguration, ok := Component(component).(ReapplyConfiguration); ok {
138+
return reapplyConfiguration, true
139+
}
140+
if reapplyConfiguration, ok := getSpec(component).(ReapplyConfiguration); ok {
141+
return reapplyConfiguration, true
142+
}
143+
return nil, false
144+
}
145+
121146
// Implement the PlacementConfiguration interface.
122147
func (s *PlacementSpec) GetDeploymentNamespace() string {
123148
return s.Namespace
@@ -197,6 +222,14 @@ func (s *TypeSpec) GetAdditionalManagedTypes() []reconciler.TypeInfo {
197222
return s.AdditionalManagedTypes
198223
}
199224

225+
// Implement the ReapplyConfiguration interface.
226+
func (s *ReapplySpec) GetReapplyInterval() time.Duration {
227+
if s.ReapplyInterval != nil {
228+
return s.ReapplyInterval.Duration
229+
}
230+
return time.Duration(0)
231+
}
232+
200233
// Check if state is Ready.
201234
func (s *Status) IsReady() bool {
202235
// caveat: this operates only on the status, so it does not check that observedGeneration == generation

pkg/component/reconciler.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ const (
7878
ReadyConditionReasonDeletionProcessing = "DeletionProcessing"
7979

8080
triggerBufferSize = 1024
81+
82+
defaultReapplyInterval = 60 * time.Minute
8183
)
8284

8385
// TODO: should we pass cluster.Client to hooks instead of just client.Client?
@@ -124,6 +126,8 @@ type ReconcilerOptions struct {
124126
// Whether namespaces are auto-created if missing.
125127
// If unspecified, MissingNamespacesPolicyCreate is assumed.
126128
MissingNamespacesPolicy *reconciler.MissingNamespacesPolicy
129+
// Interval after which an object will be force-reapplied, even if it seems to be synced.
130+
ReapplyInterval *time.Duration
127131
// SchemeBuilder allows to define additional schemes to be made available in the
128132
// target client.
129133
SchemeBuilder types.SchemeBuilder
@@ -182,6 +186,9 @@ func NewReconciler[T Component](name string, resourceGenerator manifests.Generat
182186
if options.MissingNamespacesPolicy == nil {
183187
options.MissingNamespacesPolicy = ref(reconciler.MissingNamespacesPolicyCreate)
184188
}
189+
if options.ReapplyInterval == nil {
190+
options.ReapplyInterval = ref(defaultReapplyInterval)
191+
}
185192

186193
return &Reconciler[T]{
187194
name: name,
@@ -796,6 +803,7 @@ func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.Reconcile
796803
UpdatePolicy: r.options.UpdatePolicy,
797804
DeletePolicy: r.options.DeletePolicy,
798805
MissingNamespacesPolicy: r.options.MissingNamespacesPolicy,
806+
ReapplyInterval: r.options.ReapplyInterval,
799807
StatusAnalyzer: r.statusAnalyzer,
800808
Metrics: reconciler.ReconcilerMetrics{
801809
ReadCounter: metrics.Operations.WithLabelValues(r.controllerName, "read"),
@@ -822,5 +830,10 @@ func (r *Reconciler[T]) getOptionsForComponent(component T) reconciler.Reconcile
822830
if typeConfiguration, ok := assertTypeConfiguration(component); ok {
823831
options.AdditionalManagedTypes = typeConfiguration.GetAdditionalManagedTypes()
824832
}
833+
if reapplyConfiguration, ok := assertReapplyConfiguration(component); ok {
834+
if reapplyInterval := reapplyConfiguration.GetReapplyInterval(); reapplyInterval > 0 {
835+
options.ReapplyInterval = &reapplyInterval
836+
}
837+
}
825838
return options
826839
}

pkg/component/types.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,23 @@ type ImpersonationConfiguration interface {
6767
// tweaking the requeue interval (by default, it would be 10 minutes).
6868
type RequeueConfiguration interface {
6969
// Get requeue interval. Should be greater than 1 minute.
70+
// A return value of zero means to use the framework default.
7071
GetRequeueInterval() time.Duration
7172
}
7273

7374
// The RetryConfiguration interface is meant to be implemented by components (or their spec) which offer
7475
// tweaking the retry interval (by default, it would be the value of the requeue interval).
7576
type RetryConfiguration interface {
7677
// Get retry interval. Should be greater than 1 minute.
78+
// A return value of zero means to use the framework default.
7779
GetRetryInterval() time.Duration
7880
}
7981

8082
// The TimeoutConfiguration interface is meant to be implemented by components (or their spec) which offer
8183
// tweaking the processing timeout (by default, it would be the value of the requeue interval).
8284
type TimeoutConfiguration interface {
8385
// Get timeout. Should be greater than 1 minute.
86+
// A return value of zero means to use the framework default.
8487
GetTimeout() time.Duration
8588
}
8689

@@ -112,6 +115,15 @@ type TypeConfiguration interface {
112115
GetAdditionalManagedTypes() []reconciler.TypeInfo
113116
}
114117

118+
// The ReapplyConfiguration interface is meant to be implemented by components (or their spec) which allow
119+
// to tune the force-reapply interval.
120+
type ReapplyConfiguration interface {
121+
// Get force-reapply interval. Should be greater than the effective requeue interval. If a value smaller than the
122+
// effective requeue interval is specified, the force-reapply might be delayed until the requeue happens.
123+
// A return value of zero means to use the framework default.
124+
GetReapplyInterval() time.Duration
125+
}
126+
115127
// +kubebuilder:object:generate=true
116128

117129
// Legacy placement spec. Components may include this into their spec.
@@ -222,6 +234,16 @@ var _ TypeConfiguration = &TypeSpec{}
222234

223235
// +kubebuilder:object:generate=true
224236

237+
// ReapplySpec allows to specify the force-reapply interval.
238+
// Components providing ReapplyConfiguration may include this into their spec.
239+
type ReapplySpec struct {
240+
ReapplyInterval *metav1.Duration `json:"reapplyInterval,omitempty"`
241+
}
242+
243+
var _ ReapplyConfiguration = &ReapplySpec{}
244+
245+
// +kubebuilder:object:generate=true
246+
225247
// Component Status. Components must include this into their status.
226248
type Status struct {
227249
ObservedGeneration int64 `json:"observedGeneration"`

pkg/component/zz_generated.deepcopy.go

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

pkg/reconciler/reconciler.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const (
5959
)
6060

6161
const (
62-
forceReapplyPeriod = 60 * time.Minute
62+
defaultReapplyInterval = 60 * time.Minute
6363
)
6464

6565
var adoptionPolicyByAnnotation = map[string]AdoptionPolicy{
@@ -116,6 +116,8 @@ type ReconcilerOptions struct {
116116
// a typical example of such additional managed types are CRDs which are implicitly created
117117
// by the workloads of the component, but not part of the manifests.
118118
AdditionalManagedTypes []TypeInfo
119+
// Interval after which an object will be force-reapplied, even if it seems to be synced.
120+
ReapplyInterval *time.Duration
119121
// How to analyze the state of the dependent objects.
120122
// If unspecified, an optimized kstatus based implementation is used.
121123
StatusAnalyzer status.StatusAnalyzer
@@ -145,13 +147,15 @@ type Reconciler struct {
145147
deletePolicy DeletePolicy
146148
missingNamespacesPolicy MissingNamespacesPolicy
147149
additionalManagedTypes []TypeInfo
150+
reapplyInterval time.Duration
148151
labelKeyOwnerId string
149152
annotationKeyOwnerId string
150153
annotationKeyDigest string
151154
annotationKeyAdoptionPolicy string
152155
annotationKeyReconcilePolicy string
153156
annotationKeyUpdatePolicy string
154157
annotationKeyDeletePolicy string
158+
annotationKeyReapplyInterval string
155159
annotationKeyApplyOrder string
156160
annotationKeyPurgeOrder string
157161
annotationKeyDeleteOrder string
@@ -180,6 +184,9 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
180184
if options.MissingNamespacesPolicy == nil {
181185
options.MissingNamespacesPolicy = ref(MissingNamespacesPolicyCreate)
182186
}
187+
if options.ReapplyInterval == nil {
188+
options.ReapplyInterval = ref(defaultReapplyInterval)
189+
}
183190
if options.StatusAnalyzer == nil {
184191
options.StatusAnalyzer = status.NewStatusAnalyzer(name)
185192
}
@@ -196,13 +203,15 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
196203
deletePolicy: *options.DeletePolicy,
197204
missingNamespacesPolicy: *options.MissingNamespacesPolicy,
198205
additionalManagedTypes: options.AdditionalManagedTypes,
206+
reapplyInterval: *options.ReapplyInterval,
199207
labelKeyOwnerId: name + "/" + types.LabelKeySuffixOwnerId,
200208
annotationKeyOwnerId: name + "/" + types.AnnotationKeySuffixOwnerId,
201209
annotationKeyDigest: name + "/" + types.AnnotationKeySuffixDigest,
202210
annotationKeyAdoptionPolicy: name + "/" + types.AnnotationKeySuffixAdoptionPolicy,
203211
annotationKeyReconcilePolicy: name + "/" + types.AnnotationKeySuffixReconcilePolicy,
204212
annotationKeyUpdatePolicy: name + "/" + types.AnnotationKeySuffixUpdatePolicy,
205213
annotationKeyDeletePolicy: name + "/" + types.AnnotationKeySuffixDeletePolicy,
214+
annotationKeyReapplyInterval: name + "/" + types.AnnotationKeySuffixReapplyInterval,
206215
annotationKeyApplyOrder: name + "/" + types.AnnotationKeySuffixApplyOrder,
207216
annotationKeyPurgeOrder: name + "/" + types.AnnotationKeySuffixPurgeOrder,
208217
annotationKeyDeleteOrder: name + "/" + types.AnnotationKeySuffixDeleteOrder,
@@ -223,7 +232,7 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
223232
// An update of an existing object will be performed if it is considered to be out of sync; that means:
224233
// - the object's manifest has changed, and the effective reconcile policy is ReconcilePolicyOnObjectChange or ReconcilePolicyOnObjectOrComponentChange or
225234
// - the specified component has changed and the effective reconcile policy is ReconcilePolicyOnObjectOrComponentChange or
226-
// - periodically after forceReapplyPeriod.
235+
// - periodically after the specified force-reapply interval.
227236
//
228237
// The update itself will be done as follows:
229238
// - if the effective update policy is UpdatePolicyReplace, a http PUT request will be sent to the Kubernetes API
@@ -348,6 +357,9 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
348357
if _, err := r.getDeletePolicy(object); err != nil {
349358
return false, errors.Wrapf(err, "error validating object %s", types.ObjectKeyToString(object))
350359
}
360+
if _, err := r.getReapplyInterval(object); err != nil {
361+
return false, errors.Wrapf(err, "error validating object %s", types.ObjectKeyToString(object))
362+
}
351363
if _, err := r.getApplyOrder(object); err != nil {
352364
return false, errors.Wrapf(err, "error validating object %s", types.ObjectKeyToString(object))
353365
}
@@ -377,6 +389,10 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
377389
// note: this must() is ok because we checked the generated objects above, and this function will be called for these objects only
378390
return must(r.getDeletePolicy(object))
379391
}
392+
getReapplyInterval := func(object client.Object) time.Duration {
393+
// note: this must() is ok because we checked the generated objects above, and this function will be called for these objects only
394+
return must(r.getReapplyInterval(object))
395+
}
380396
getApplyOrder := func(object client.Object) int {
381397
// note: this must() is ok because we checked the generated objects above, and this function will be called for these objects only
382398
return must(r.getApplyOrder(object))
@@ -692,6 +708,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
692708
setAnnotation(object, r.annotationKeyDigest, item.Digest)
693709

694710
updatePolicy := getUpdatePolicy(object)
711+
reapplyInterval := getReapplyInterval(object)
695712
now := time.Now()
696713
if existingObject == nil {
697714
if err := r.createObject(ctx, object, nil, updatePolicy); err != nil {
@@ -702,8 +719,7 @@ func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, obj
702719
item.LastAppliedAt = &metav1.Time{Time: now}
703720
numUnready++
704721
} else if existingObject.GetDeletionTimestamp().IsZero() &&
705-
// TODO: make force-reconcile period (60 minutes as of now) configurable
706-
(existingObject.GetAnnotations()[r.annotationKeyDigest] != item.Digest || item.LastAppliedAt == nil || item.LastAppliedAt.Time.Before(now.Add(-forceReapplyPeriod))) {
722+
(existingObject.GetAnnotations()[r.annotationKeyDigest] != item.Digest || item.LastAppliedAt == nil || item.LastAppliedAt.Time.Before(now.Add(-reapplyInterval))) {
707723
switch updatePolicy {
708724
case UpdatePolicyRecreate:
709725
if err := r.deleteObject(ctx, object, existingObject, hashedOwnerId); err != nil {
@@ -1343,6 +1359,18 @@ func (r *Reconciler) getDeletePolicy(object client.Object) (DeletePolicy, error)
13431359
}
13441360
}
13451361

1362+
func (r *Reconciler) getReapplyInterval(object client.Object) (time.Duration, error) {
1363+
value, ok := object.GetAnnotations()[r.annotationKeyReapplyInterval]
1364+
if !ok {
1365+
return r.reapplyInterval, nil
1366+
}
1367+
reapplyInterval, err := time.ParseDuration(value)
1368+
if err != nil {
1369+
return 0, errors.Wrapf(err, "invalid value for annotation %s: %s", r.annotationKeyReapplyInterval, value)
1370+
}
1371+
return reapplyInterval, nil
1372+
}
1373+
13461374
func (r *Reconciler) getApplyOrder(object client.Object) (int, error) {
13471375
value, ok := object.GetAnnotations()[r.annotationKeyApplyOrder]
13481376
if !ok {

pkg/types/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
AnnotationKeySuffixReconcilePolicy = "reconcile-policy"
1414
AnnotationKeySuffixUpdatePolicy = "update-policy"
1515
AnnotationKeySuffixDeletePolicy = "delete-policy"
16+
AnnotationKeySuffixReapplyInterval = "reapply-interval"
1617
AnnotationKeySuffixApplyOrder = "apply-order"
1718
AnnotationKeySuffixPurgeOrder = "purge-order"
1819
AnnotationKeySuffixDeleteOrder = "delete-order"

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ To support such cases, the `Generator` implementation can set the following anno
4444
- `mycomponent-operator.mydomain.io/apply-order`: the wave in which this object will be reconciled; dependents will be reconciled wave by wave; that is, objects of the same wave will be deployed in a canonical order, and the reconciler will only proceed to the next wave if all objects of previous waves are ready; specified orders can be negative or positive numbers between -32768 and 32767, objects with no explicit order set are treated as if they would specify order 0
4545
- `mycomponent-operator.mydomain.io/purge-order` (optional): the wave by which this object will be purged; here, purged means that, while applying the dependents, the object will be deleted from the cluster at the end of the specified wave; the according record in `status.Inventory` will be set to phase `Completed`; setting purge orders is useful to spawn ad-hoc objects during the reconcilation, which are not permanently needed; so it's comparable to Helm hooks, in a certain sense
4646
- `mycomponent-operator.mydomain.io/delete-order` (optional): the wave by which this object will be deleted; that is, if the dependent is no longer part of the component, or if the whole component is being deleted; dependents will be deleted wave by wave; that is, objects of the same wave will be deleted in a canonical order, and the reconciler will only proceed to the next wave if all objects of previous saves are gone; specified orders can be negative or positive numbers between -32768 and 32767, objects with no explicit order set are treated as if they would specify order 0; note that the delete order is completely independent of the apply order
47+
- `mycomponent-operator.mydomain.io/reapply-interval` (optional): the interval after which a force-reapply of the object will be performed (even it is in sync otherwise); if not specified, the reconciler default is used; note that, even if the specified force-reapply interval has passed, the next reconcile may happen only after the current requeue interval is over; because of that, it makes sense to set the reapply interval to a value (significantly) larger than the effective requeue interval.
4748
- `mycomponent-operator.mydomain.io/status-hint` (optional): a comma-separated list of hints that may help the framework to properly identify the state of the annotated dependent object; currently, the following hints are possible:
4849
- `has-observed-generation`: tells the framework that the dependent object has a `status.observedGeneration` field, even if it is not (yet) set by the responsible controller (some controllers are known to set the observed generation lazily, with the consequence that there is a period right after creation of the dependent object, where the field is missing in the dependent's status)
4950
- `has-ready-condition`: tells the framework to count with a ready condition; if it is absent, the condition status will be considered as `Unknown`

0 commit comments

Comments
 (0)