/
childset.go
436 lines (377 loc) · 15.3 KB
/
childset.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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
/*
Copyright 2023 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package reconcilers
import (
"context"
"errors"
"fmt"
"sync"
"github.com/go-logr/logr"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"reconciler.io/runtime/internal"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var (
_ SubReconciler[client.Object] = (*ChildSetReconciler[client.Object, client.Object, client.ObjectList])(nil)
)
// ChildSetReconciler is a sub reconciler that manages a set of child resources for a reconciled
// resource. A correlation ID is used to track the desired state of each child resource across
// reconcile requests. A ChildReconciler is created dynamically and reconciled for each desired
// and discovered child resource.
//
// During setup, the child resource type is registered to watch for changes.
type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.ObjectList] struct {
// Name used to identify this reconciler. Defaults to `{ChildType}ChildSetReconciler`. Ideally
// unique, but not required to be so.
//
// +optional
Name string
// ChildType is the resource being created/updated/deleted by the reconciler. For example, a
// reconciled resource Deployment would have a ReplicaSet as a child. Required when the
// generic type is not a struct, or is unstructured.
//
// +optional
ChildType ChildType
// ChildListType is the listing type for the child type. For example,
// PodList is the list type for Pod. Required when the generic type is not
// a struct, or is unstructured.
//
// +optional
ChildListType ChildListType
// Finalizer is set on the reconciled resource before a child resource is created, and cleared
// after a child resource is deleted. The value must be unique to this specific reconciler
// instance and not shared. Reusing a value may result in orphaned resources when the
// reconciled resource is deleted.
//
// Using a finalizer is encouraged when the Kubernetes garbage collector is unable to delete
// the child resource automatically, like when the reconciled resource and child are in different
// namespaces, scopes or clusters.
//
// Use of a finalizer implies that SkipOwnerReference is true.
//
// +optional
Finalizer string
// SkipOwnerReference when true will not create and find child resources via an owner
// reference. OurChild must be defined for the reconciler to distinguish the child being
// reconciled from other resources of the same type.
//
// Any child resource created is tracked for changes.
SkipOwnerReference bool
// Setup performs initialization on the manager and builder this reconciler
// will run with. It's common to setup field indexes and watch resources.
//
// +optional
Setup func(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error
// DesiredChildren returns the set of desired child object for the given reconciled resource,
// or nil if no children should exist. Each resource returned from this method must be claimed
// by the OurChild method with a stable, unique identifier returned. The identifier is used to
// correlate desired and actual child resources.
//
// Current known children can be obtained via RetrieveKnownChildren[ChildType](ctx). This can
// be used to keep existing children while stamping out new resources, or for garbage
// collecting resources based on some criteria. Return the children that should be kept and
// omit children to delete.
//
// To skip reconciliation of the child resources while still reflecting an existing child's
// status on the reconciled resource, return OnlyReconcileChildStatus as an error.
DesiredChildren func(ctx context.Context, resource Type) ([]ChildType, error)
// ReflectChildrenStatusOnParent updates the reconciled resource's status with values from the
// child reconciliations. Select types of errors are captured, including:
// - apierrs.IsAlreadyExists
// - apierrs.IsInvalid
//
// Most errors are returned directly, skipping this method. The set of handled error types
// may grow, implementations should be defensive rather than assuming the error type.
//
// Results contain the union of desired and actual child resources, in the order they were
// reconciled, (sorted by identifier).
ReflectChildrenStatusOnParent func(ctx context.Context, parent Type, result ChildSetResult[ChildType])
// HarmonizeImmutableFields allows fields that are immutable on the current
// object to be copied to the desired object in order to avoid creating
// updates which are guaranteed to fail.
//
// +optional
HarmonizeImmutableFields func(current, desired ChildType)
// MergeBeforeUpdate copies desired fields on to the current object before
// calling update. Typically fields to copy are the Spec, Labels and
// Annotations.
MergeBeforeUpdate func(current, desired ChildType)
// ListOptions allows custom options to be use when listing potential child resources. Each
// resource retrieved as part of the listing is confirmed via OurChild. There is a performance
// benefit to limiting the number of resource return for each List operation, however,
// excluding an actual child will orphan that resource.
//
// Defaults to filtering by the reconciled resource's namespace:
// []client.ListOption{
// client.InNamespace(resource.GetNamespace()),
// }
//
// ListOptions is required when a Finalizer is defined or SkipOwnerReference is true. An empty
// list is often sufficient although it may incur a performance penalty, especially when
// querying the API sever instead of an informer cache.
//
// +optional
ListOptions func(ctx context.Context, resource Type) []client.ListOption
// OurChild is used when there are multiple sources of children of the same ChildType
// controlled by the same reconciled resource. The function return true for child resources
// managed by this ChildReconciler. Objects returned from the DesiredChildren function should
// match this function, otherwise they may be orphaned. If not specified, all children match.
// Matched child resources must also be uniquely identifiable with the IdentifyChild method.
//
// OurChild is required when a Finalizer is defined or SkipOwnerReference is true.
//
// +optional
OurChild func(resource Type, child ChildType) bool
// IdentifyChild returns a stable identifier for the child resource. The identifier is used to
// correlate desired child resources with actual child resources. The same value must be returned
// for an object both before and after it is created on the API server.
//
// Non-deterministic IDs will result in the rapid deletion and creation of child resources.
IdentifyChild func(child ChildType) string
// Sanitize is called with an object before logging the value. Any value may
// be returned. A meaningful subset of the resource is typically returned,
// like the Spec.
//
// +optional
Sanitize func(child ChildType) interface{}
stamp *ResourceManager[ChildType]
lazyInit sync.Once
voidReconciler *ChildReconciler[Type, ChildType, ChildListType]
}
func (r *ChildSetReconciler[T, CT, CLT]) init() {
r.lazyInit.Do(func() {
var nilCT CT
if internal.IsNil(r.ChildType) {
r.ChildType = newEmpty(nilCT).(CT)
}
if internal.IsNil(r.ChildListType) {
var nilCLT CLT
r.ChildListType = newEmpty(nilCLT).(CLT)
}
if r.Name == "" {
r.Name = fmt.Sprintf("%sChildSetReconciler", typeName(r.ChildType))
}
r.stamp = &ResourceManager[CT]{
Name: r.Name,
Type: r.ChildType,
TrackDesired: r.SkipOwnerReference,
HarmonizeImmutableFields: r.HarmonizeImmutableFields,
MergeBeforeUpdate: r.MergeBeforeUpdate,
Sanitize: r.Sanitize,
}
r.voidReconciler = r.childReconcilerFor(nilCT, nil, "", true)
})
}
func (r *ChildSetReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr ctrl.Manager, bldr *builder.Builder) error {
r.init()
c := RetrieveConfigOrDie(ctx)
log := logr.FromContextOrDiscard(ctx).
WithName(r.Name).
WithValues("childType", gvk(c, r.ChildType))
ctx = logr.NewContext(ctx, log)
if err := r.validate(ctx); err != nil {
return err
}
if err := r.voidReconciler.SetupWithManager(ctx, mgr, bldr); err != nil {
return err
}
if r.Setup == nil {
return nil
}
return r.Setup(ctx, mgr, bldr)
}
func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredErr error, id string, void bool) *ChildReconciler[T, CT, CLT] {
return &ChildReconciler[T, CT, CLT]{
Name: id,
ChildType: r.ChildType,
ChildListType: r.ChildListType,
SkipOwnerReference: r.SkipOwnerReference,
DesiredChild: func(ctx context.Context, resource T) (CT, error) {
return desired, desiredErr
},
ReflectChildStatusOnParent: func(ctx context.Context, parent T, child CT, err error) {
result := retrieveChildSetResult[CT](ctx)
result.Children = append(result.Children, ChildSetPartialResult[CT]{
Id: id,
Child: child,
Err: err,
})
stashChildSetResult(ctx, result)
},
HarmonizeImmutableFields: r.HarmonizeImmutableFields,
MergeBeforeUpdate: r.MergeBeforeUpdate,
ListOptions: r.ListOptions,
OurChild: func(resource T, child CT) bool {
if r.OurChild != nil && !r.OurChild(resource, child) {
return false
}
return void || id == r.IdentifyChild(child)
},
Sanitize: r.Sanitize,
}
}
func (r *ChildSetReconciler[T, CT, CLT]) validate(ctx context.Context) error {
// default implicit values
if r.Finalizer != "" {
r.SkipOwnerReference = true
}
// require DesiredChildren
if r.DesiredChildren == nil {
return fmt.Errorf("ChildSetReconciler %q must implement DesiredChildren", r.Name)
}
// require ReflectChildrenStatusOnParent
if r.ReflectChildrenStatusOnParent == nil {
return fmt.Errorf("ChildSetReconciler %q must implement ReflectChildrenStatusOnParent", r.Name)
}
if r.OurChild == nil && r.SkipOwnerReference {
// OurChild is required when SkipOwnerReference is true
return fmt.Errorf("ChildSetReconciler %q must implement OurChild since owner references are not used", r.Name)
}
if r.ListOptions == nil && r.SkipOwnerReference {
// ListOptions is required when SkipOwnerReference is true
return fmt.Errorf("ChildSetReconciler %q must implement ListOptions since owner references are not used", r.Name)
}
// require IdentifyChild
if r.IdentifyChild == nil {
return fmt.Errorf("ChildSetReconciler %q must implement IdentifyChild", r.Name)
}
return nil
}
func (r *ChildSetReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T) (Result, error) {
r.init()
log := logr.FromContextOrDiscard(ctx).
WithName(r.Name)
ctx = logr.NewContext(ctx, log)
knownChildren, err := r.knownChildren(ctx, resource)
if err != nil {
return Result{}, err
}
ctx = stashKnownChildren(ctx, knownChildren)
cr, err := r.composeChildReconcilers(ctx, resource, knownChildren)
if err != nil {
return Result{}, err
}
result, err := cr.Reconcile(ctx, resource)
r.reflectStatus(ctx, resource)
return result, err
}
func (r *ChildSetReconciler[T, CT, CLT]) knownChildren(ctx context.Context, resource T) ([]CT, error) {
c := RetrieveConfigOrDie(ctx)
children := r.ChildListType.DeepCopyObject().(CLT)
ourChildren := []CT{}
if err := c.List(ctx, children, r.voidReconciler.listOptions(ctx, resource)...); err != nil {
return nil, err
}
for _, child := range extractItems[CT](children) {
if !r.voidReconciler.ourChild(resource, child) {
continue
}
ourChildren = append(ourChildren, child.DeepCopyObject().(CT))
}
return ourChildren, nil
}
func (r *ChildSetReconciler[T, CT, CLT]) composeChildReconcilers(ctx context.Context, resource T, knownChildren []CT) (SubReconciler[T], error) {
desiredChildren, desiredChildrenErr := r.DesiredChildren(ctx, resource)
if desiredChildrenErr != nil && !errors.Is(desiredChildrenErr, OnlyReconcileChildStatus) {
return nil, desiredChildrenErr
}
childIDs := sets.NewString()
desiredChildByID := map[string]CT{}
for _, child := range desiredChildren {
id := r.IdentifyChild(child)
if id == "" {
return nil, fmt.Errorf("desired child id may not be empty")
}
if childIDs.Has(id) {
return nil, fmt.Errorf("duplicate child id found: %s", id)
}
childIDs.Insert(id)
desiredChildByID[id] = child
}
for _, child := range knownChildren {
id := r.IdentifyChild(child)
childIDs.Insert(id)
}
sequence := Sequence[T]{}
for _, id := range childIDs.List() {
child := desiredChildByID[id]
cr := r.childReconcilerFor(child, desiredChildrenErr, id, false)
cr.SetResourceManager(r.stamp)
sequence = append(sequence, cr)
}
if r.Finalizer != "" {
return &WithFinalizer[T]{
Finalizer: r.Finalizer,
Reconciler: sequence,
}, nil
}
return sequence, nil
}
func (r *ChildSetReconciler[T, CT, CLT]) reflectStatus(ctx context.Context, parent T) {
result := clearChildSetResult[CT](ctx)
r.ReflectChildrenStatusOnParent(ctx, parent, result)
}
type ChildSetResult[T client.Object] struct {
Children []ChildSetPartialResult[T]
}
type ChildSetPartialResult[T client.Object] struct {
Id string
Child T
Err error
}
func (r *ChildSetResult[T]) AggregateError() error {
var errs []error
for _, childResult := range r.Children {
errs = append(errs, childResult.Err)
}
return utilerrors.NewAggregate(errs)
}
const childSetResultStashKey StashKey = "reconciler.io/runtime:childSetResult"
func retrieveChildSetResult[T client.Object](ctx context.Context) ChildSetResult[T] {
value := RetrieveValue(ctx, childSetResultStashKey)
if result, ok := value.(ChildSetResult[T]); ok {
return result
}
return ChildSetResult[T]{}
}
func stashChildSetResult[T client.Object](ctx context.Context, result ChildSetResult[T]) {
StashValue(ctx, childSetResultStashKey, result)
}
func clearChildSetResult[T client.Object](ctx context.Context) ChildSetResult[T] {
value := ClearValue(ctx, childSetResultStashKey)
if result, ok := value.(ChildSetResult[T]); ok {
return result
}
return ChildSetResult[T]{}
}
const knownChildrenStashKey StashKey = "reconciler.io/runtime:knownChildren"
// RetrieveKnownChildren returns the children managed by current ChildSetReconciler. The known
// children can be returned from the DesiredChildren method to preserve existing children, or to
// mutate/delete an existing child.
//
// For example, a child stamper could be implemented by returning existing children from
// DesiredChildren and appending an addition child when a new resource should be created. Likewise
// existing children can be garbage collected by omitting a known child.
func RetrieveKnownChildren[T client.Object](ctx context.Context) []T {
value := ctx.Value(knownChildrenStashKey)
if result, ok := value.([]T); ok {
return result
}
return nil
}
func stashKnownChildren[T client.Object](ctx context.Context, children []T) context.Context {
return context.WithValue(ctx, knownChildrenStashKey, children)
}