Skip to content

Commit 2ac95df

Browse files
committed
WIP: document runtime patch package
Signed-off-by: Hidde Beydals <hello@hidde.co>
1 parent 1835cdc commit 2ac95df

File tree

3 files changed

+99
-15
lines changed

3 files changed

+99
-15
lines changed

runtime/patch/doc.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2021 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package patch implements patch utilities to help with proper patching of objects while reducing the number of
18+
// potential conflicts.
19+
package patch

runtime/patch/options.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,17 @@ type Option interface {
2525

2626
// HelperOptions contains options for patch options.
2727
type HelperOptions struct {
28-
// IncludeStatusObservedGeneration sets the status.observedGeneration field
29-
// on the incoming object to match metadata.generation, only if there is a change.
28+
// IncludeStatusObservedGeneration sets the status.observedGeneration field on the incoming object to match
29+
// metadata.generation, only if there is a change.
3030
IncludeStatusObservedGeneration bool
3131

3232
// ForceOverwriteConditions allows the patch helper to overwrite conditions in case of conflicts.
3333
// This option should only ever be set in controller managing the object being patched.
3434
ForceOverwriteConditions bool
3535

3636
// OwnedConditions defines condition types owned by the controller.
37-
// In case of conflicts for the owned conditions, the patch helper will always use the value provided by the controller.
37+
// In case of conflicts for the owned conditions, the patch helper will always use the value provided by the
38+
// controller.
3839
OwnedConditions []string
3940
}
4041

@@ -47,8 +48,8 @@ func (w WithForceOverwriteConditions) ApplyToHelper(in *HelperOptions) {
4748
in.ForceOverwriteConditions = true
4849
}
4950

50-
// WithStatusObservedGeneration sets the status.observedGeneration field
51-
// on the incoming object to match metadata.generation, only if there is a change.
51+
// WithStatusObservedGeneration sets the status.observedGeneration field on the incoming object to match
52+
// metadata.generation, only if there is a change.
5253
type WithStatusObservedGeneration struct{}
5354

5455
// ApplyToHelper applies this configuration to the given HelperOptions.

runtime/patch/patch.go

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
// }
39102
type 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.
169232
func (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.
245309
func (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.
269333
func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error) {
270334
// Calculate patch data.
271335
patch := client.MergeFrom(h.beforeObject)

0 commit comments

Comments
 (0)