@@ -36,6 +36,69 @@ import (
3636)
3737
3838// Helper is a utility for ensuring the proper patching of objects.
39+ //
40+ // The Helper MUST be initialised before a set of modifications within the scope of an envisioned patch are made
41+ // to an object, so that the difference in state can be utilised to calculate a patch that can be used on a new revision
42+ // of the resource in case of conflicts.
43+ //
44+ // A common pattern for reconcilers is to initialise a NewHelper at the beginning of their Reconcile method, after
45+ // having fetched the latest revision for the resource from the API server, and then defer the call of Helper.Patch.
46+ // This ensures any modifications made to the spec and the status (conditions) object of the resource are always
47+ // persisted at the end of a reconcile run.
48+ //
49+ // func (r *FooReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
50+ // // Retrieve the object from the API server
51+ // obj := &v1.Foo{}
52+ // if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
53+ // return ctrl.Result{}, client.IgnoreNotFound(err)
54+ // }
55+ //
56+ // // Initialize the patch helper
57+ // patchHelper, err := patch.NewHelper(obj, r.Client)
58+ // if err != nil {
59+ // return ctrl.Result{}, err
60+ // }
61+ //
62+ // // Always attempt to patch the object and status after each reconciliation
63+ // defer func() {
64+ // // Patch the object, ignoring conflicts on the conditions owned by this controller
65+ // patchOpts := []patch.Option{
66+ // patch.WithOwnedConditions{
67+ // Conditions: []string{
68+ // meta.ReadyCondition,
69+ // meta.ReconcilingCondition,
70+ // meta.ProgressingReason,
71+ // // any other "owned conditions"
72+ // },
73+ // },
74+ // }
75+ //
76+ // // Determine if the resource is still being reconciled, or if it has stalled, and record this observation
77+ // if retErr == nil && (result.IsZero() || !result.Requeue) {
78+ // conditions.Delete(obj, meta.ReconcilingCondition)
79+ //
80+ // // We have now observed this generation
81+ // patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
82+ //
83+ // readyCondition := conditions.Get(obj, meta.ReadyCondition)
84+ // switch readyCondition.Status {
85+ // case metav1.ConditionFalse:
86+ // // As we are no longer reconciling and the end-state is not ready, the reconciliation has stalled
87+ // conditions.MarkTrue(obj, meta.StalledCondition, readyCondition.Reason, readyCondition.Message)
88+ // case metav1.ConditionTrue:
89+ // // As we are no longer reconciling and the end-state is ready, the reconciliation is no longer stalled
90+ // conditions.Delete(obj, meta.StalledCondition)
91+ // }
92+ // }
93+ //
94+ // // Finally, patch the resource
95+ // if err := patchHelper.Patch(ctx, obj, patchOpts...); err != nil {
96+ // retErr = kerrors.NewAggregate([]error{retErr, err})
97+ // }
98+ // }()
99+ //
100+ // // ...start with actual reconciliation logic
101+ // }
39102type Helper struct {
40103 client client.Client
41104 gvk schema.GroupVersionKind
@@ -157,15 +220,15 @@ func (h *Helper) patchStatus(ctx context.Context, obj client.Object) error {
157220 return h .client .Status ().Patch (ctx , afterObject , client .MergeFrom (beforeObject ))
158221}
159222
160- // patchStatusConditions issues a patch if there are any changes to the conditions slice under
161- // the status subresource. This is a special case and it's handled separately given that
162- // we allow different controllers to act on conditions of the same object.
223+ // patchStatusConditions issues a patch if there are any changes to the conditions slice under the status subresource.
224+ // This is a special case and it's handled separately given that we allow different controllers to act on conditions of
225+ // the same object.
163226//
164- // This method has an internal backoff loop. When a conflict is detected, the method
165- // asks the Client for the a new version of the object we're trying to patch.
227+ // This method has an internal backoff loop. When a conflict is detected, the method asks the Client for the a new
228+ // version of the object we're trying to patch.
166229//
167- // Condition changes are then applied to the latest version of the object, and if there are
168- // no unresolvable conflicts, the patch is sent again.
230+ // Condition changes are then applied to the latest version of the object, and if there are no unresolvable conflicts,
231+ // the patch is sent again.
169232func (h * Helper ) patchStatusConditions (ctx context.Context , obj client.Object , forceOverwrite bool , ownedConditions []string ) error {
170233 // Nothing to do if the object isn't a condition patcher.
171234 if ! h .isConditionsSetter {
@@ -241,7 +304,8 @@ func (h *Helper) patchStatusConditions(ctx context.Context, obj client.Object, f
241304 })
242305}
243306
244- // calculatePatch returns the before/after objects to be given in a controller-runtime patch, scoped down to the absolute necessary.
307+ // calculatePatch returns the before/after objects to be given in a controller-runtime patch, scoped down to the
308+ // absolute necessary.
245309func (h * Helper ) calculatePatch (afterObj client.Object , focus patchType ) (client.Object , client.Object , error ) {
246310 // Get a shallow unsafe copy of the before/after object in unstructured form.
247311 before := unsafeUnstructuredCopy (h .before , focus , h .isConditionsSetter )
@@ -264,8 +328,8 @@ func (h *Helper) shouldPatch(in string) bool {
264328 return h .changes [in ]
265329}
266330
267- // calculate changes tries to build a patch from the before/after objects we have
268- // and store in a map which top-level fields (e.g. `metadata`, `spec`, `status`, etc.) have changed.
331+ // calculate changes tries to build a patch from the before/after objects we have and store in a map which top-level
332+ // fields (e.g. `metadata`, `spec`, `status`, etc.) have changed.
269333func (h * Helper ) calculateChanges (after client.Object ) (map [string ]bool , error ) {
270334 // Calculate patch data.
271335 patch := client .MergeFrom (h .beforeObject )
0 commit comments