Skip to content

Commit fea0031

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 3c4deba commit fea0031

File tree

4 files changed

+114
-0
lines changed

4 files changed

+114
-0
lines changed

pkg/config/v1alpha1/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ type ControllerManagerConfigurationSpec struct {
5555
// The graceful shutdown is skipped for safety reasons in case the leader election lease is lost.
5656
GracefulShutdownTimeout *metav1.Duration `json:"gracefulShutDown,omitempty"`
5757

58+
// PrestartTimeout is the duration given to each prestart hook before they return successfully.
59+
// To use prestart hooks without a timeout, set to a negative duration, e.g. time.Duration(-1)
60+
PrestartTimeout *metav1.Duration `json:"prestartTimeout,omitempty"`
61+
5862
// Controller contains global configuration options for controllers
5963
// registered within this manager.
6064
// +optional

pkg/manager/internal.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656
defaultRenewDeadline = 10 * time.Second
5757
defaultRetryPeriod = 2 * time.Second
5858
defaultGracefulShutdownPeriod = 30 * time.Second
59+
defaultPrestartPeriod = 15 * time.Second
5960

6061
defaultReadinessEndpoint = "/readyz"
6162
defaultLivenessEndpoint = "/healthz"
@@ -176,6 +177,13 @@ type controllerManager struct {
176177
// internalProceduresStop channel is used internally to the manager when coordinating
177178
// the proper shutdown of servers. This channel is also used for dependency injection.
178179
internalProceduresStop chan struct{}
180+
181+
// prestartHooks are functions that are run immediately before calling the Start functions
182+
// of the leader election runnables.
183+
prestartHooks []func(ctx context.Context) error
184+
185+
// prestartTimeout is the duration given to each prestart hook to return successfully.
186+
prestartTimeout time.Duration
179187
}
180188

181189
type hasCache interface {
@@ -272,6 +280,19 @@ func (cm *controllerManager) AddReadyzCheck(name string, check healthz.Checker)
272280
return nil
273281
}
274282

283+
// AddPrestartHook allows you to add prestart hooks.
284+
func (cm *controllerManager) AddPrestartHook(hook func(ctx context.Context) error) error {
285+
cm.Lock()
286+
defer cm.Unlock()
287+
288+
if cm.started {
289+
return fmt.Errorf("unable to add new prestart hook because the manager has already been started")
290+
}
291+
292+
cm.prestartHooks = append(cm.prestartHooks, hook)
293+
return nil
294+
}
295+
275296
func (cm *controllerManager) GetConfig() *rest.Config {
276297
return cm.cluster.GetConfig()
277298
}
@@ -611,6 +632,18 @@ func (cm *controllerManager) engageStopProcedure(stopComplete <-chan struct{}) e
611632
}
612633

613634
func (cm *controllerManager) startLeaderElectionRunnables() error {
635+
cm.logger.Info("Starting prestart hooks")
636+
for _, hook := range cm.prestartHooks {
637+
ctx, cancel := context.WithTimeout(cm.internalCtx, cm.prestartTimeout)
638+
if err := hook(ctx); err != nil {
639+
return err
640+
}
641+
cancel()
642+
}
643+
644+
// All the prestart hooks have been run, clear the slice to free the underlying resources.
645+
cm.prestartHooks = nil
646+
614647
return cm.runnables.LeaderElection.Start(cm.internalCtx)
615648
}
616649

pkg/manager/manager.go

Lines changed: 20 additions & 0 deletions
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
//
@@ -299,6 +305,10 @@ type Options struct {
299305
// +optional
300306
Controller v1alpha1.ControllerConfigurationSpec
301307

308+
// PrestartTimeout is the duration given to each prestart hook to return successfully.
309+
// To use prestart hooks without timeout, set to a negative duration, e.g. time.Duration(-1)
310+
PrestartTimeout *time.Duration
311+
302312
// makeBroadcaster allows deferring the creation of the broadcaster to
303313
// avoid leaking goroutines if we never call Start on this manager. It also
304314
// returns whether or not this is a "owned" broadcaster, and as such should be
@@ -447,6 +457,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
447457
readinessEndpointName: options.ReadinessEndpointName,
448458
livenessEndpointName: options.LivenessEndpointName,
449459
gracefulShutdownTimeout: *options.GracefulShutdownTimeout,
460+
prestartTimeout: *options.PrestartTimeout,
450461
internalProceduresStop: make(chan struct{}),
451462
leaderElectionStopped: make(chan struct{}),
452463
leaderElectionReleaseOnCancel: options.LeaderElectionReleaseOnCancel,
@@ -507,6 +518,10 @@ func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options,
507518
o.CertDir = newObj.Webhook.CertDir
508519
}
509520

521+
if o.PrestartTimeout == nil && newObj.PrestartTimeout != nil {
522+
o.PrestartTimeout = &newObj.PrestartTimeout.Duration
523+
}
524+
510525
if newObj.Controller != nil {
511526
if o.Controller.CacheSyncTimeout == nil && newObj.Controller.CacheSyncTimeout != nil {
512527
o.Controller.CacheSyncTimeout = newObj.Controller.CacheSyncTimeout
@@ -644,6 +659,11 @@ func setOptionsDefaults(options Options) Options {
644659
options.GracefulShutdownTimeout = &gracefulShutdownTimeout
645660
}
646661

662+
if options.PrestartTimeout == nil {
663+
prestartTimeout := defaultPrestartPeriod
664+
options.PrestartTimeout = &prestartTimeout
665+
}
666+
647667
if options.Logger.GetSink() == nil {
648668
options.Logger = log.Log
649669
}

pkg/manager/manager_test.go

Lines changed: 57 additions & 0 deletions
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)