-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathreconciler.go
343 lines (313 loc) · 16.4 KB
/
reconciler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package operator
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"k8s.io/utils/strings/slices"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/resource"
)
// ReconcileAction describes the action that triggered reconciliation.
type ReconcileAction int
const (
// ReconcileActionUnknown represents an Unknown ReconcileAction
ReconcileActionUnknown ReconcileAction = iota
// ReconcileActionCreated indicates that the resource to reconcile has been created.
// Note that this action may also be used on initial start-up of some informer-based implementations,
// such as the KubernetesBasedInformer. To instead receive Resync actions for these events,
// use the OpinionatedReconciler.
ReconcileActionCreated
// ReconcileActionUpdated indicates that the resource to reconcile has been updated.
ReconcileActionUpdated
// ReconcileActionDeleted indicates that the resource to reconcile has been deleted.
// Note that if the resource has Finalizers attached to it, a ReconcileActionUpdated will be used to indicate
// "tombstoning" of the resource where DeletionTimestamp is non-nil and Finalizers may only be removed.
// On completion of the actual delete from the API server once the Finalizers list is empty,
// a Delete reconcile action will be triggered.
ReconcileActionDeleted
// ReconcileActionResynced indicates a periodic or initial re-sync of existing resources in the API server.
// Note that not all implementations support this action (KubernetesBasedInformer will only trigger Created,
// Updated, and Deleted actions. You can use OpinionatedReconciler to introduce Resync events on start instead
// of Add events).
ReconcileActionResynced
)
// ReconcileRequest contains the action which took place, and a snapshot of the object at that point in time.
// The Object in the ReconcileRequest is not guaranteed to be the current state of the object in-storage,
// as other actions may have taken place subsequently.
//
// Controllers such as InformerController contain logic to dequeue ReconcileRequests if subsequent actions
// are received for the same object.
type ReconcileRequest struct {
// Action is the action that triggered this ReconcileRequest
Action ReconcileAction
// Object is the object at the time of the received action
Object resource.Object
// State is a user-defined map of state values that can be provided on retried ReconcileRequests.
// See State in ReconcileResult. It will always be nil on an initial Reconcile call,
// and will only be non-nil if a prior Reconcile call with this ReconcileRequest returned a State
// in its ReconcileResult alongside either a RequeueAfter or an error.
State map[string]any
}
// ReconcileResult is the status of a successful Reconcile action.
// "Success" in this case simply indicates that unexpected errors did not occur,
// as the ReconcileResult can specify that the Reconcile action should be re-queued to run again
// after a period of time has elapsed.
type ReconcileResult struct {
// RequeueAfter is a duration after which the Reconcile action which returned this result should be retried.
// If nil, the Reconcile action will not be requeued.
RequeueAfter *time.Duration
// State can be used alongside RequeueAfter to add the provided state map to the ReconcileRequest supplied in the
// future Reconcile call. This allows a Reconcile to "partially complete" and not have to re-do tasks
// if it needs to wait on an additional bit of information or if a particular call results in a transient failure.
State map[string]any
}
// Reconciler is an interface which describes an object which implements simple Reconciliation behavior.
type Reconciler interface {
// Reconcile should be called whenever any action is received for a relevant object.
// The action and object at the time the action was received are contained within the ReconcileRequest.
// If the returned ReconcileResult has a non-nil RequeueAfter, the managing controller should requeue
// the Reconcile action, with the same ReconcileRequest and context, after that duration has elapsed.
// If the call returns an error, the Reconcile action should be requeued according to the retry policy
// of the controller.
Reconcile(ctx context.Context, req ReconcileRequest) (ReconcileResult, error)
}
// ReconcileActionFromResourceAction returns the equivalent ReconcileAction from a provided ResourceAction.
// If there is no equivalent, it returns ReconcileActionUnknown.
func ReconcileActionFromResourceAction(action ResourceAction) ReconcileAction {
switch action {
case ResourceActionCreate:
return ReconcileActionCreated
case ResourceActionUpdate:
return ReconcileActionUpdated
case ResourceActionDelete:
return ReconcileActionDeleted
default:
return ReconcileActionUnknown
}
}
// ResourceActionFromReconcileAction returns the equivalent ResourceAction from a provided ReconcileAction.
// If there is no equivalent, it returns an empty ResourceAction.
func ResourceActionFromReconcileAction(action ReconcileAction) ResourceAction {
switch action {
case ReconcileActionCreated:
return ResourceActionCreate
case ReconcileActionUpdated:
return ResourceActionUpdate
case ReconcileActionDeleted:
return ResourceActionDelete
default:
return ResourceAction("")
}
}
// NewOpinionatedReconciler creates a new OpinionatedReconciler.
// To have the new OpinionatedReconciler wrap an existing reconciler,
// set the `OpinionatedReconciler.Reconciler` value or use `OpinionatedReconciler.Wrap()`
func NewOpinionatedReconciler(client PatchClient, finalizer string) (*OpinionatedReconciler, error) {
if client == nil {
return nil, fmt.Errorf("client cannot be nil")
}
if finalizer == "" {
return nil, fmt.Errorf("finalizer cannot be empty")
}
if len(finalizer) > 63 {
return nil, fmt.Errorf("finalizer length cannot exceed 63 chars: %s", finalizer)
}
return &OpinionatedReconciler{
finalizer: finalizer,
client: client,
}, nil
}
// OpinionatedReconciler wraps an ordinary Reconciler with finalizer-based logic to convert "Created" events into
// "resync" events on start-up when the reconciler has handled the "created" event on a previous run,
// and ensures that "delete" events are not missed during reconciler down-time by using the finalizer.
type OpinionatedReconciler struct {
Reconciler Reconciler
finalizer string
client PatchClient
}
const (
opinionatedReconcilerPatchAddStateKey = "grafana-app-sdk-opinionated-reconciler-create-patch-status"
opinionatedReconcilerPatchRemoveStateKey = "grafana-app-sdk-opinionated-reconciler-delete-patch-status"
)
// Reconcile consumes a ReconcileRequest and passes it off to the underlying ReconcileFunc, using the following criteria to modify or drop the request if needed:
// - If the action is a Create, and the OpinionatedReconciler's finalizer is in the finalizer list, update the action to a Resync
// - If the action is a Create, and the OpinionatedReconciler's finalizer is missing, add the finalizer after the delegated Reconcile request returns successfully
// - If the action is an Update, and the DeletionTimestamp is non-nil, remove the OpinionatedReconciler's finalizer, and do not delegate (the subsequent Delete will be delegated)
// - If the action is an Update, and the OpinionatedReconciler's finalizer is missing (and DeletionTimestamp is nil), add the finalizer, and do not delegate (the subsequent update action will delegate)
//
//nolint:funlen
func (o *OpinionatedReconciler) Reconcile(ctx context.Context, request ReconcileRequest) (ReconcileResult, error) {
ctx, span := GetTracer().Start(ctx, "OpinionatedReconciler-reconcile")
defer span.End()
logger := logging.FromContext(ctx).With("action", ResourceActionFromReconcileAction(request.Action), "component", "OpinionatedReconciler", "kind", request.Object.GroupVersionKind().Kind, "namespace", request.Object.GetNamespace(), "name", request.Object.GetName())
logger.Debug("Reconcile request received")
// Check if this action is a create, and the resource already has a finalizer. If so, make it a sync.
if request.Action == ReconcileActionCreated && slices.Contains(request.Object.GetFinalizers(), o.finalizer) {
logger.Debug("Object already has the correct finalizer, converting to a Resync event and propagating", "finalizer", o.finalizer)
request.Action = ReconcileActionResynced
return o.wrappedReconcile(ctx, request)
}
if request.Action == ReconcileActionCreated {
resp := ReconcileResult{}
if request.State == nil || request.State[opinionatedReconcilerPatchAddStateKey] == nil {
// Delegate
var err error
resp, err = o.wrappedReconcile(ctx, request)
if err != nil || resp.RequeueAfter != nil {
if resp.RequeueAfter != nil {
span.SetAttributes(attribute.String("reconcile.requeafter", resp.RequeueAfter.String()))
}
if err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("watcher add error: %s", err.Error()))
}
return resp, err
}
}
// Attach the finalizer on success
logger.Debug("Downstream reconcile succeeded, adding finalizer", "finalizer", o.finalizer)
patchErr := o.client.PatchInto(ctx, request.Object.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpAdd,
Path: "/metadata/finalizers",
Value: []string{o.finalizer},
}},
}, resource.PatchOptions{}, request.Object)
if patchErr != nil {
span.SetStatus(codes.Error, fmt.Sprintf("error adding finalizer: %s", patchErr.Error()))
if resp.State == nil {
resp.State = make(map[string]any)
}
resp.State[opinionatedReconcilerPatchAddStateKey] = patchErr
}
return resp, patchErr
}
// Ignore deleted actions, as we send them on updates where we need to remove our finalizer
// This needs to be checked before the "is deletionTimestamp non-nil and still has our finalizer",
// because after we remove the finalizer, a delete event comes through that still has the final finalizer to be removed from the list
if request.Action == ReconcileActionDeleted {
logger.Debug("Not propagating delete event, as this is handled when deletionTimestamp is set to a non-nil value")
return ReconcileResult{}, nil
}
if request.Object.GetDeletionTimestamp() != nil && slices.Contains(request.Object.GetFinalizers(), o.finalizer) {
res := ReconcileResult{}
if request.State == nil || request.State[opinionatedReconcilerPatchRemoveStateKey] == nil {
logger.Debug("Update added a deletionTimestamp, propagate this event as a delete", "deletionTimestamp", request.Object.GetDeletionTimestamp())
// Propagate as a delete, if the delete succeeds, remove the finalizer
request.Action = ReconcileActionDeleted
var err error
res, err = o.wrappedReconcile(ctx, request)
if err != nil || res.RequeueAfter != nil {
if res.RequeueAfter != nil {
span.SetAttributes(attribute.String("reconcile.requeafter", res.RequeueAfter.String()))
}
if err != nil {
span.SetStatus(codes.Error, fmt.Sprintf("watcher add error: %s", err.Error()))
}
return res, err
}
} else {
logger.Debug("Retry of an update which added a deletionTimestamp, downstream reconciler already successfully processed delete, need to retry removing the finalizer", "patchError", request.State[opinionatedReconcilerPatchRemoveStateKey])
}
logger.Debug("Removing finalizer from object", "finalizer", o.finalizer)
patchErr := o.client.PatchInto(ctx, request.Object.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpRemove,
Path: fmt.Sprintf("/metadata/finalizers/%d", slices.Index(request.Object.GetFinalizers(), o.finalizer)),
}},
}, resource.PatchOptions{}, request.Object)
if patchErr != nil {
span.SetStatus(codes.Error, fmt.Sprintf("error adding finalizer: %s", patchErr.Error()))
if res.State == nil {
res.State = make(map[string]any)
}
res.State[opinionatedReconcilerPatchRemoveStateKey] = patchErr
}
return res, patchErr
}
if request.Object.GetDeletionTimestamp() != nil {
logger.Debug("Object has a deletionTimestamp but does not contain our finalizer, ignoring event as object delete has already been processed", "finalizer", o.finalizer, "deletionTimestamp", request.Object.GetDeletionTimestamp())
return ReconcileResult{}, nil
}
if request.Action == ReconcileActionUpdated && !slices.Contains(request.Object.GetFinalizers(), o.finalizer) {
// Add the finalizer, don't delegate, let the reconcile action for adding the finalizer propagate down to avoid confusing extra reconciliations
logger.Debug("Missing finalizer in object, adding (this will trigger a new reconcile event)", "finalizer", o.finalizer)
patchErr := o.client.PatchInto(ctx, request.Object.GetStaticMetadata().Identifier(), resource.PatchRequest{
Operations: []resource.PatchOperation{{
Operation: resource.PatchOpAdd,
Path: "/metadata/finalizers",
Value: []string{o.finalizer},
}},
}, resource.PatchOptions{}, request.Object)
return ReconcileResult{}, patchErr
}
return o.wrappedReconcile(ctx, request)
}
func (o *OpinionatedReconciler) wrappedReconcile(ctx context.Context, request ReconcileRequest) (ReconcileResult, error) {
if o.Reconciler != nil {
return o.Reconciler.Reconcile(ctx, request)
}
return ReconcileResult{}, nil
}
// Wrap wraps the provided Reconciler's Reconcile function with this OpinionatedReconciler
func (o *OpinionatedReconciler) Wrap(reconciler Reconciler) {
o.Reconciler = reconciler
}
// Compile-time interface compliance check
var _ Reconciler = &OpinionatedReconciler{}
// SimpleReconciler is a simple Reconciler implementation that calls ReconcileFunc if non-nil on Reconcile requests.
type SimpleReconciler struct {
ReconcileFunc func(context.Context, ReconcileRequest) (ReconcileResult, error)
}
// Reconcile calls ReconcileFunc if non-nil and returns the response, or returns an empty ReconcileResult and nil error
// if ReconcileFunc is nil.
func (s *SimpleReconciler) Reconcile(ctx context.Context, req ReconcileRequest) (ReconcileResult, error) {
if s.ReconcileFunc != nil {
return s.ReconcileFunc(ctx, req)
}
return ReconcileResult{}, nil
}
// Compile-time interface compliance check
var _ Reconciler = &SimpleReconciler{}
// TypedReconcileRequest is a variation of ReconcileRequest which uses a concretely-typed Object,
// rather than the interface resource.Object. It is used by TypedReconciler in its ReconcileFunc.
type TypedReconcileRequest[T resource.Object] struct {
// Action is the actions which triggered this TypedReconcileRequest
Action ReconcileAction
// Object is the object on which the Action was performed, at the point in time of that Action
Object T
// State is a user-defined map of state values that can be provided on retried ReconcileRequests.
// See State in ReconcileResult. It will always be nil on an initial Reconcile call,
// and will only be non-nil if a prior Reconcile call with this TypedReconcileRequest returned a State
// in its ReconcileResult alongside either a RequeueAfter or an error.
State map[string]any
}
// TypedReconciler is a variant of SimpleReconciler in which a user can specify the underlying type of the resource.Object
// which is in the provided ReconcileRequest. Reconcile() will then attempt to cast the resource.Object in the
// ReconcileRequest into the provided T type and produce a TypedReconcileRequest, which will be passed to ReconcileFunc.
type TypedReconciler[T resource.Object] struct {
// ReconcileFunc is called by TypedReconciler.Reconcile using the T-typed Object instead of a resource.Object.
ReconcileFunc func(context.Context, TypedReconcileRequest[T]) (ReconcileResult, error)
}
// Reconcile tries to cast the Object in ReconcileRequest into the T-typed resource.Object,
// then creates a TypedReconcileRequest with the cast object and the same Action and State,
// which is passed to ReconcileFunc. If the Object cannot be cast, it returns an empty
// ReconcileResult with an error of type *CannotCastError. If ReconcileFunc is nil,
// it returns an empty ReconcileResult with a nil error.
func (t *TypedReconciler[T]) Reconcile(ctx context.Context, request ReconcileRequest) (ReconcileResult, error) {
if t.ReconcileFunc == nil {
return ReconcileResult{}, nil
}
cast, ok := request.Object.(T)
if !ok {
return ReconcileResult{}, NewCannotCastError(request.Object.GetStaticMetadata())
}
return t.ReconcileFunc(ctx, TypedReconcileRequest[T]{
Action: request.Action,
Object: cast,
State: request.State,
})
}
// Compile-time interface compliance check
var _ Reconciler = &TypedReconciler[resource.Object]{}