Skip to content

Commit a457e27

Browse files
authored
Merge pull request #863 from negz/stop
✨Allow controllers to be started and stopped separately from the manager
2 parents 6501aeb + f779bdd commit a457e27

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

pkg/controller/controller.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ type Controller interface {
6767
// New returns a new Controller registered with the Manager. The Manager will ensure that shared Caches have
6868
// been synced before the Controller is Started.
6969
func New(name string, mgr manager.Manager, options Options) (Controller, error) {
70+
c, err := NewUnmanaged(name, mgr, options)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
// Add the controller as a Manager components
76+
return c, mgr.Add(c)
77+
}
78+
79+
// NewUnmanaged returns a new controller without adding it to the manager. The
80+
// caller is responsible for starting the returned controller.
81+
func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller, error) {
7082
if options.Reconciler == nil {
7183
return nil, fmt.Errorf("must specify Reconciler")
7284
}
@@ -100,9 +112,9 @@ func New(name string, mgr manager.Manager, options Options) (Controller, error)
100112
return workqueue.NewNamedRateLimitingQueue(options.RateLimiter, name)
101113
},
102114
MaxConcurrentReconciles: options.MaxConcurrentReconciles,
115+
SetFields: mgr.SetFields,
103116
Name: name,
104117
}
105118

106-
// Add the controller as a Manager components
107-
return c, mgr.Add(c)
119+
return c, nil
108120
}

pkg/controller/example_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,48 @@ func ExampleController_unstructured() {
119119
os.Exit(1)
120120
}
121121
}
122+
123+
// This example creates a new controller named "pod-controller" to watch Pods
124+
// and call a no-op reconciler. The controller is not added to the provided
125+
// manager, and must thus be started and stopped by the caller.
126+
func ExampleNewUnmanaged() {
127+
// mgr is a manager.Manager
128+
129+
// Configure creates a new controller but does not add it to the supplied
130+
// manager.
131+
c, err := controller.NewUnmanaged("pod-controller", mgr, controller.Options{
132+
Reconciler: reconcile.Func(func(_ reconcile.Request) (reconcile.Result, error) {
133+
return reconcile.Result{}, nil
134+
}),
135+
})
136+
if err != nil {
137+
log.Error(err, "unable to create pod-controller")
138+
os.Exit(1)
139+
}
140+
141+
if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForObject{}); err != nil {
142+
log.Error(err, "unable to watch pods")
143+
os.Exit(1)
144+
}
145+
146+
// Create a stop channel for our controller. The controller will stop when
147+
// this channel is closed.
148+
stop := make(chan struct{})
149+
150+
// Start our controller in a goroutine so that we do not block.
151+
go func() {
152+
// Block until our controller manager is elected leader. We presume our
153+
// entire process will terminate if we lose leadership, so we don't need
154+
// to handle that.
155+
<-mgr.Elected()
156+
157+
// Start our controller. This will block until the stop channel is
158+
// closed, or the controller returns an error.
159+
if err := c.Start(stop); err != nil {
160+
log.Error(err, "cannot run experiment controller")
161+
}
162+
}()
163+
164+
// Stop our controller.
165+
close(stop)
166+
}

pkg/manager/internal.go

+12
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ type controllerManager struct {
134134
// It and `internalStop` should point to the same channel.
135135
internalStopper chan<- struct{}
136136

137+
// elected is closed when this manager becomes the leader of a group of
138+
// managers, either because it won a leader election or because no leader
139+
// election was configured.
140+
elected chan struct{}
141+
137142
startCache func(stop <-chan struct{}) error
138143

139144
// port is the port that the webhook server serves at.
@@ -457,6 +462,8 @@ func (cm *controllerManager) Start(stop <-chan struct{}) error {
457462
return err
458463
}
459464
} else {
465+
// Treat not having leader election enabled the same as being elected.
466+
close(cm.elected)
460467
go cm.startLeaderElectionRunnables()
461468
}
462469

@@ -545,6 +552,7 @@ func (cm *controllerManager) startLeaderElection() (err error) {
545552
RetryPeriod: cm.retryPeriod,
546553
Callbacks: leaderelection.LeaderCallbacks{
547554
OnStartedLeading: func(_ context.Context) {
555+
close(cm.elected)
548556
cm.startLeaderElectionRunnables()
549557
},
550558
OnStoppedLeading: func() {
@@ -572,3 +580,7 @@ func (cm *controllerManager) startLeaderElection() (err error) {
572580
go l.Run(ctx)
573581
return nil
574582
}
583+
584+
func (cm *controllerManager) Elected() <-chan struct{} {
585+
return cm.elected
586+
}

pkg/manager/manager.go

+6
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ type Manager interface {
5151
// non-leaderelection mode (always running) or leader election mode (managed by leader election if enabled).
5252
Add(Runnable) error
5353

54+
// Elected is closed when this manager is elected leader of a group of
55+
// managers, either because it won a leader election or because no leader
56+
// election was configured.
57+
Elected() <-chan struct{}
58+
5459
// SetFields will set any dependencies on an object for which the object has implemented the inject
5560
// interface - e.g. inject.Client.
5661
SetFields(interface{}) error
@@ -325,6 +330,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
325330
metricsExtraHandlers: metricsExtraHandlers,
326331
internalStop: stop,
327332
internalStopper: stop,
333+
elected: make(chan struct{}),
328334
port: options.Port,
329335
host: options.Host,
330336
certDir: options.CertDir,

pkg/manager/manager_test.go

+55-2
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ var _ = Describe("manger.Manager", func() {
143143
Context("with leader election enabled", func() {
144144
It("should default ID to controller-runtime if ID is not set", func() {
145145
var rl resourcelock.Interface
146-
m, err := New(cfg, Options{
146+
m1, err := New(cfg, Options{
147147
LeaderElection: true,
148148
LeaderElectionNamespace: "default",
149149
LeaderElectionID: "test-leader-election-id",
@@ -152,10 +152,61 @@ var _ = Describe("manger.Manager", func() {
152152
rl, err = leaderelection.NewResourceLock(config, recorderProvider, options)
153153
return rl, err
154154
},
155+
HealthProbeBindAddress: "0",
156+
MetricsBindAddress: "0",
155157
})
156158
Expect(err).ToNot(HaveOccurred())
157-
Expect(m).ToNot(BeNil())
159+
Expect(m1).ToNot(BeNil())
158160
Expect(rl.Describe()).To(Equal("default/test-leader-election-id"))
161+
162+
m2, err := New(cfg, Options{
163+
LeaderElection: true,
164+
LeaderElectionNamespace: "default",
165+
LeaderElectionID: "test-leader-election-id",
166+
newResourceLock: func(config *rest.Config, recorderProvider recorder.Provider, options leaderelection.Options) (resourcelock.Interface, error) {
167+
var err error
168+
rl, err = leaderelection.NewResourceLock(config, recorderProvider, options)
169+
return rl, err
170+
},
171+
HealthProbeBindAddress: "0",
172+
MetricsBindAddress: "0",
173+
})
174+
175+
Expect(err).ToNot(HaveOccurred())
176+
Expect(m2).ToNot(BeNil())
177+
Expect(rl.Describe()).To(Equal("default/test-leader-election-id"))
178+
179+
c1 := make(chan struct{})
180+
Expect(m1.Add(RunnableFunc(func(s <-chan struct{}) error {
181+
defer GinkgoRecover()
182+
close(c1)
183+
return nil
184+
}))).To(Succeed())
185+
186+
go func() {
187+
defer GinkgoRecover()
188+
Expect(m1.Elected()).ShouldNot(BeClosed())
189+
Expect(m1.Start(stop)).NotTo(HaveOccurred())
190+
Expect(m1.Elected()).Should(BeClosed())
191+
}()
192+
<-c1
193+
194+
c2 := make(chan struct{})
195+
Expect(m2.Add(RunnableFunc(func(s <-chan struct{}) error {
196+
defer GinkgoRecover()
197+
close(c2)
198+
return nil
199+
}))).To(Succeed())
200+
201+
By("Expect second manager to lose leader election")
202+
go func() {
203+
defer GinkgoRecover()
204+
Expect(m2.Start(stop)).NotTo(HaveOccurred())
205+
Consistently(m2.Elected()).ShouldNot(Receive())
206+
}()
207+
208+
By("Expect controller on manager without leader lease never to run")
209+
Consistently(c2).ShouldNot(Receive())
159210
})
160211

161212
It("should return an error if namespace not set and not running in cluster", func() {
@@ -260,7 +311,9 @@ var _ = Describe("manger.Manager", func() {
260311

261312
go func() {
262313
defer GinkgoRecover()
314+
Expect(m.Elected()).ShouldNot(BeClosed())
263315
Expect(m.Start(stop)).NotTo(HaveOccurred())
316+
Expect(m.Elected()).Should(BeClosed())
264317
}()
265318
<-c1
266319
<-c2

0 commit comments

Comments
 (0)