@@ -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.
116119func (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.
317326func (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.
325339func (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.
333352func (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.
341365func (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.
349378func (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().
356389func (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
384428func (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