Skip to content

Commit b507b92

Browse files
committed
reconcile: add some locking to prevent out-of-order or problematic concurrent calls
1 parent 3435a45 commit b507b92

File tree

1 file changed

+53
-4
lines changed

1 file changed

+53
-4
lines changed

pkg/component/reconcile.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"regexp"
1515
"strconv"
1616
"strings"
17+
"sync"
1718
"time"
1819

1920
"github.com/pkg/errors"
@@ -95,6 +96,8 @@ type Reconciler[T Component] struct {
9596
postReconcileHooks []HookFunc[T]
9697
preDeleteHooks []HookFunc[T]
9798
postDeleteHooks []HookFunc[T]
99+
setupMutex sync.Mutex
100+
setupComplete bool
98101
}
99102

100103
// Create a new Reconciler.
@@ -114,7 +117,13 @@ func NewReconciler[T Component](name string, client client.Client, discoveryClie
114117

115118
// Reconcile contains the actual reconciliation logic.
116119
func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
117-
// TODO: add some check (and lock?) to prevent Reconcile() to be invoked before SetupWithManager() was called
120+
r.setupMutex.Lock()
121+
if !r.setupComplete {
122+
defer r.setupMutex.Unlock()
123+
panic("usage error: SetupWithManger() must be called first")
124+
}
125+
r.setupMutex.Unlock()
126+
118127
log := log.FromContext(ctx)
119128
log.V(1).Info("running reconcile")
120129

@@ -315,6 +324,11 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
315324
// Register post-read hook with reconciler.
316325
// This hook will be called after the reconciled component object has been retrieved from the Kubernetes API.
317326
func (r *Reconciler[T]) WithPostReadHook(hook HookFunc[T]) *Reconciler[T] {
327+
r.setupMutex.Lock()
328+
defer r.setupMutex.Unlock()
329+
if r.setupComplete {
330+
panic("usage error: hooks can only be registered before SetupWithManger() was called")
331+
}
318332
r.postReadHooks = append(r.postReadHooks, hook)
319333
return r
320334
}
@@ -323,6 +337,11 @@ func (r *Reconciler[T]) WithPostReadHook(hook HookFunc[T]) *Reconciler[T] {
323337
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
324338
// right before the reconcilation of the dependent objects starts.
325339
func (r *Reconciler[T]) WithPreReconcileHook(hook HookFunc[T]) *Reconciler[T] {
340+
r.setupMutex.Lock()
341+
defer r.setupMutex.Unlock()
342+
if r.setupComplete {
343+
panic("usage error: hooks can only be registered before SetupWithManger() was called")
344+
}
326345
r.preReconcileHooks = append(r.preReconcileHooks, hook)
327346
return r
328347
}
@@ -331,6 +350,11 @@ func (r *Reconciler[T]) WithPreReconcileHook(hook HookFunc[T]) *Reconciler[T] {
331350
// This hook will be called if the reconciled component is not in deletion (has no deletionTimestamp set),
332351
// right after the reconcilation of the dependent objects happened, and was successful.
333352
func (r *Reconciler[T]) WithPostReconcileHook(hook HookFunc[T]) *Reconciler[T] {
353+
r.setupMutex.Lock()
354+
defer r.setupMutex.Unlock()
355+
if r.setupComplete {
356+
panic("usage error: hooks can only be registered before SetupWithManger() was called")
357+
}
334358
r.postReconcileHooks = append(r.postReconcileHooks, hook)
335359
return r
336360
}
@@ -339,6 +363,11 @@ func (r *Reconciler[T]) WithPostReconcileHook(hook HookFunc[T]) *Reconciler[T] {
339363
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
340364
// right before the deletion of the dependent objects starts.
341365
func (r *Reconciler[T]) WithPreDeleteHook(hook HookFunc[T]) *Reconciler[T] {
366+
r.setupMutex.Lock()
367+
defer r.setupMutex.Unlock()
368+
if r.setupComplete {
369+
panic("usage error: hooks can only be registered before SetupWithManger() was called")
370+
}
342371
r.preDeleteHooks = append(r.preDeleteHooks, hook)
343372
return r
344373
}
@@ -347,14 +376,23 @@ func (r *Reconciler[T]) WithPreDeleteHook(hook HookFunc[T]) *Reconciler[T] {
347376
// This hook will be called if the reconciled component is in deletion (has a deletionTimestamp set),
348377
// right after the deletion of the dependent objects happened, and was successful.
349378
func (r *Reconciler[T]) WithPostDeleteHook(hook HookFunc[T]) *Reconciler[T] {
379+
r.setupMutex.Lock()
380+
defer r.setupMutex.Unlock()
381+
if r.setupComplete {
382+
panic("usage error: hooks can only be registered before SetupWithManger() was called")
383+
}
350384
r.postDeleteHooks = append(r.postDeleteHooks, hook)
351385
return r
352386
}
353387

354388
// Register the reconciler with a given controller-runtime Manager.
355-
// TODO: probably we could merge NewReconciler() and SetupWithManager() into one function, such as SetupReconciler().
356389
func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error {
357-
// TODO: add some check (and lock?) to prevent SetupWithManager() being called more than once
390+
r.setupMutex.Lock()
391+
defer r.setupMutex.Unlock()
392+
if r.setupComplete {
393+
panic("usage error: SetupWithManger() must not be called more than once")
394+
}
395+
358396
kubeSystemNamespace := &corev1.Namespace{}
359397
if err := mgr.GetAPIReader().Get(context.Background(), apitypes.NamespacedName{Name: "kube-system"}, kubeSystemNamespace); err != nil {
360398
return errors.Wrap(err, "error retrieving uid of kube-system namespace")
@@ -374,11 +412,17 @@ func (r *Reconciler[T]) SetupWithManager(mgr ctrl.Manager) error {
374412
}
375413

376414
component := newComponent[T]()
377-
return ctrl.NewControllerManagedBy(mgr).
415+
err = ctrl.NewControllerManagedBy(mgr).
378416
For(component).
379417
WithEventFilter(predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{})).
380418
WithOptions(controller.Options{MaxConcurrentReconciles: 3}).
381419
Complete(r)
420+
if err != nil {
421+
return errors.Wrap(err, "error creating controller")
422+
}
423+
424+
r.setupComplete = true
425+
return nil
382426
}
383427

384428
func (r *Reconciler[T]) getClientForComponent(component T) (cluster.Client, error) {
@@ -472,6 +516,11 @@ func (t *reconcileTarget[T]) Reconcile(ctx context.Context, component T) (bool,
472516
status := component.GetStatus()
473517

474518
// render manifests
519+
// TODO: sometimes the generator needs more information about the rendered component (such as the component's name or namespace);
520+
// as of now, components have to help themselves through implementing post-read hooks; to simplify this for components,
521+
// we could expose the component (full object or maybe just its metadata) via the generate context;
522+
// another option would be to have a special NamespacedName reuse type, where the namespace part would be auto-defaulted
523+
// by the framework (similar to the auto-loading of configmap/secret references)
475524
generateCtx := manifests.NewContextWithClient(manifests.NewContextWithReconcilerName(ctx, t.reconcilerName), t.client)
476525
objects, err := t.resourceGenerator.Generate(generateCtx, namespace, name, component.GetSpec())
477526
if err != nil {

0 commit comments

Comments
 (0)