Skip to content

Commit 3b513d7

Browse files
committed
feat(manager): 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 manager implementation. This hook runs after the manager has been elected leader, immediately before the leader election controllers are started. Related #607
1 parent af8d903 commit 3b513d7

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed

pkg/manager/internal.go

+26
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ type controllerManager struct {
173173
// internalProceduresStop channel is used internally to the manager when coordinating
174174
// the proper shutdown of servers. This channel is also used for dependency injection.
175175
internalProceduresStop chan struct{}
176+
177+
// prestartHooks are functions that are run immediately before calling the Start functions
178+
// of the leader election runnables.
179+
prestartHooks []func(ctx context.Context) error
176180
}
177181

178182
type hasCache interface {
@@ -269,6 +273,19 @@ func (cm *controllerManager) AddReadyzCheck(name string, check healthz.Checker)
269273
return nil
270274
}
271275

276+
// AddPrestartHook allows you to add prestart hooks.
277+
func (cm *controllerManager) AddPrestartHook(hook func(ctx context.Context) error) error {
278+
cm.Lock()
279+
defer cm.Unlock()
280+
281+
if cm.started {
282+
return fmt.Errorf("unable to add new prestart hook because the manager has already been started")
283+
}
284+
285+
cm.prestartHooks = append(cm.prestartHooks, hook)
286+
return nil
287+
}
288+
272289
func (cm *controllerManager) GetConfig() *rest.Config {
273290
return cm.cluster.GetConfig()
274291
}
@@ -606,6 +623,15 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
606623
}
607624

608625
func (cm *controllerManager) startLeaderElectionRunnables() error {
626+
for _, hook := range cm.prestartHooks {
627+
if err := hook(cm.internalCtx); err != nil {
628+
return err
629+
}
630+
}
631+
632+
// All the prestart hooks have been run, clear the slice to free the underlying resources.
633+
cm.prestartHooks = nil
634+
609635
return cm.runnables.LeaderElection.Start(cm.internalCtx)
610636
}
611637

pkg/manager/manager.go

+6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ type Manager interface {
7979
// AddReadyzCheck allows you to add Readyz checker
8080
AddReadyzCheck(name string, check healthz.Checker) error
8181

82+
// AddPrestartHook allows you to add a hook that runs after leader election and immediately
83+
// before starting controllers needing leader election. Prestart hooks block execution of
84+
// leader election controllers until all return nil error. The manager is stopped on non-nil
85+
// errors.
86+
AddPrestartHook(func(ctx context.Context) error) error
87+
8288
// Start starts all registered Controllers and blocks until the context is cancelled.
8389
// Returns an error if there is an error starting any controller.
8490
//

pkg/manager/manager_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,63 @@ var _ = Describe("manger.Manager", func() {
10761076
<-runnableStopped
10771077
})
10781078

1079+
It("should run prestart hooks before calling Start on leader election runnables", func() {
1080+
m, err := New(cfg, options)
1081+
Expect(err).NotTo(HaveOccurred())
1082+
for _, cb := range callbacks {
1083+
cb(m)
1084+
}
1085+
1086+
runnableRan := make(chan struct{})
1087+
1088+
Expect(m.Add(RunnableFunc(func(ctx context.Context) error {
1089+
close(runnableRan)
1090+
return nil
1091+
})))
1092+
1093+
Expect(m.AddPrestartHook(func(ctx context.Context) error {
1094+
Expect(m.Elected()).ShouldNot(BeClosed())
1095+
Consistently(runnableRan).ShouldNot(BeClosed())
1096+
return nil
1097+
}))
1098+
1099+
ctx, cancel := context.WithCancel(context.Background())
1100+
defer cancel()
1101+
go func() {
1102+
defer GinkgoRecover()
1103+
Expect(m.Elected()).ShouldNot(BeClosed())
1104+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1105+
}()
1106+
1107+
<-m.Elected()
1108+
})
1109+
1110+
It("should not run leader election runnables if prestart hooks fail", func() {
1111+
m, err := New(cfg, options)
1112+
Expect(err).NotTo(HaveOccurred())
1113+
for _, cb := range callbacks {
1114+
cb(m)
1115+
}
1116+
1117+
runnableRan := make(chan struct{})
1118+
1119+
Expect(m.Add(RunnableFunc(func(ctx context.Context) error {
1120+
close(runnableRan)
1121+
return nil
1122+
})))
1123+
1124+
Expect(m.AddPrestartHook(func(ctx context.Context) error {
1125+
Expect(m.Elected()).ShouldNot(BeClosed())
1126+
Consistently(runnableRan).ShouldNot(BeClosed())
1127+
return errors.New("prestart hook failed")
1128+
}))
1129+
1130+
ctx, cancel := context.WithCancel(context.Background())
1131+
defer cancel()
1132+
1133+
Expect(m.Elected()).ShouldNot(BeClosed())
1134+
Expect(m.Start(ctx)).Should(MatchError(ContainSubstring("prestart hook failed")))
1135+
})
10791136
}
10801137

10811138
Context("with defaults", func() {

0 commit comments

Comments
 (0)