Skip to content

Commit bebdc3e

Browse files
committed
feat(controller): add prestart hook support
When implementing a controller that uses leader election, there maybe be work that needs to be done after winning the election but before processing enqueued requests. For example, a controller may need to build up an internal mapping of the current state of the cluster before it can begin reconciling. This changeset adds support for adding prestart hooks to controller-runtime's internal controller implementation. This hook runs after the controller's caches have been synchronized, but before the reconciliation workers have been started. The `PrestartHookable` interface has been added to allow users to determine of hooks are supported. Fixes #607
1 parent 8da9760 commit bebdc3e

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

pkg/controller/controller.go

+7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ type Controller interface {
8383
GetLogger() logr.Logger
8484
}
8585

86+
// PrestartHookable is implemented by controllers that support registering prestart hooks that run
87+
// after caches have been synced (and optionally, leader election), but before their manage reconcile loop.
88+
type PrestartHookable interface {
89+
// Registers a prestart hook with the controller.
90+
PrestartHook(func(ctx context.Context) error) error
91+
}
92+
8693
// New returns a new Controller registered with the Manager. The Manager will ensure that shared Caches have
8794
// been synced before the Controller is Started.
8895
func New(name string, mgr manager.Manager, options Options) (Controller, error) {

pkg/internal/controller/controller.go

+29
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ type Controller struct {
9292

9393
// RecoverPanic indicates whether the panic caused by reconcile should be recovered.
9494
RecoverPanic bool
95+
96+
// prestartHooks are functions that are run after caches have been synced, but before the reconcile loop has
97+
// been started. This allows for work to be done after winning a leader election.
98+
prestartHooks []func(ctx context.Context) error
9599
}
96100

97101
// watchDescription contains all the information necessary to start a watch.
@@ -223,6 +227,18 @@ func (c *Controller) Start(ctx context.Context) error {
223227
// which won't be garbage collected if we hold a reference to it.
224228
c.startWatches = nil
225229

230+
c.LogConstructor(nil).Info("Running Prestart Hooks")
231+
for _, hook := range c.prestartHooks {
232+
if err := hook(ctx); err != nil {
233+
err := fmt.Errorf("failed to run prestart hook: %w", err)
234+
c.LogConstructor(nil).Error(err, "Could not run prestart hook")
235+
return err
236+
}
237+
}
238+
239+
// All the prestart hooks have been run, clear the slice to free the underlying resources.
240+
c.prestartHooks = nil
241+
226242
// Launch workers to process resources
227243
c.LogConstructor(nil).Info("Starting workers", "worker count", c.MaxConcurrentReconciles)
228244
wg.Add(c.MaxConcurrentReconciles)
@@ -354,6 +370,19 @@ func (c *Controller) InjectFunc(f inject.Func) error {
354370
return nil
355371
}
356372

373+
// PrestartHook implements controller.PrestartHookable.
374+
func (c *Controller) PrestartHook(hook func(context.Context) error) error {
375+
c.mu.Lock()
376+
defer c.mu.Unlock()
377+
378+
if !c.Started {
379+
c.prestartHooks = append(c.prestartHooks, hook)
380+
return nil
381+
}
382+
383+
return errors.New("controller has already been added")
384+
}
385+
357386
// updateMetrics updates prometheus metrics within the controller.
358387
func (c *Controller) updateMetrics(reconcileTime time.Duration) {
359388
ctrlmetrics.ReconcileTime.WithLabelValues(c.Name).Observe(reconcileTime.Seconds())

pkg/internal/controller/controller_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,69 @@ var _ = Describe("controller", func() {
455455
})
456456
})
457457

458+
Describe("PrestartHook", func() {
459+
It("should register multiple prestart hooks", func() {
460+
fn1 := func(ctx context.Context) error {
461+
return nil
462+
}
463+
fn2 := func(ctx context.Context) error {
464+
return nil
465+
}
466+
467+
Expect(ctrl.PrestartHook(fn1)).ShouldNot(HaveOccurred())
468+
Expect(ctrl.PrestartHook(fn2)).ShouldNot(HaveOccurred())
469+
Expect(ctrl.prestartHooks).Should(HaveLen(2))
470+
})
471+
472+
It("should call prestart hooks before reconciler", func() {
473+
ctx, cancel := context.WithCancel(context.Background())
474+
defer cancel()
475+
476+
ch := make(chan struct{})
477+
fn1 := func(ctx context.Context) error {
478+
Consistently(reconciled).ShouldNot(Receive())
479+
close(ch)
480+
return nil
481+
}
482+
483+
Expect(ctrl.PrestartHook(fn1)).ShouldNot(HaveOccurred())
484+
go func() {
485+
defer GinkgoRecover()
486+
Expect(ctrl.Start(ctx)).To(Succeed())
487+
}()
488+
Eventually(ch).Should(BeClosed())
489+
})
490+
491+
It("should return an error if called after start", func() {
492+
ctx, cancel := context.WithCancel(context.Background())
493+
defer cancel()
494+
495+
fn1 := func(ctx context.Context) error {
496+
return nil
497+
}
498+
499+
go func() {
500+
defer GinkgoRecover()
501+
Expect(ctrl.Start(ctx)).To(Succeed())
502+
}()
503+
504+
Eventually(func() bool { return ctrl.Started }).Should(BeTrue())
505+
Expect(ctrl.PrestartHook(fn1)).Should(HaveOccurred())
506+
})
507+
508+
It("should stop controller if hook returns error", func() {
509+
ctx, cancel := context.WithCancel(context.Background())
510+
defer cancel()
511+
512+
fn1 := func(ctx context.Context) error {
513+
return errors.New("hook error")
514+
}
515+
516+
Expect(ctrl.PrestartHook(fn1)).ShouldNot(HaveOccurred())
517+
Expect(ctrl.Start(ctx)).Should(MatchError(ContainSubstring("hook error")))
518+
})
519+
})
520+
458521
Describe("Processing queue items from a Controller", func() {
459522
It("should call Reconciler if an item is enqueued", func() {
460523
ctx, cancel := context.WithCancel(context.Background())

0 commit comments

Comments
 (0)