Skip to content

Commit d4debf9

Browse files
committed
✨ Conditionally runnable controllers
Implement conditionally runnable controllers that wait and watch for their respective resource to exist in the discovery doc before running the controller. This enables the controller manager to: * Start without error when a CRD the controller watches does not exist. * Begin watching the CRD and start its controller once it isinstalled. * Stop the controller (and the respective informer) upon CRD uninstall. * Restart the controller upon reinstallling the CRD.
1 parent 5f210d2 commit d4debf9

File tree

15 files changed

+387
-11
lines changed

15 files changed

+387
-11
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/prometheus/client_golang v1.0.0
1919
github.com/prometheus/client_model v0.2.0
2020
github.com/prometheus/procfs v0.0.11 // indirect
21+
github.com/robfig/cron v1.2.0
2122
github.com/spf13/pflag v1.0.5
2223
go.uber.org/atomic v1.4.0 // indirect
2324
go.uber.org/zap v1.10.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,8 @@ github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNG
298298
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
299299
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
300300
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
301+
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
302+
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
301303
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
302304
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
303305
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=

pkg/builder/controller.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package builder
1919
import (
2020
"fmt"
2121
"strings"
22+
"time"
2223

2324
"github.com/go-logr/logr"
2425
"k8s.io/apimachinery/pkg/runtime"
@@ -68,8 +69,10 @@ func (blder *Builder) ForType(apiType runtime.Object) *Builder {
6869

6970
// ForInput represents the information set by For method.
7071
type ForInput struct {
71-
object runtime.Object
72-
predicates []predicate.Predicate
72+
object runtime.Object
73+
predicates []predicate.Predicate
74+
conditionallyRun bool
75+
waitTime time.Duration
7376
}
7477

7578
// For defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete /
@@ -263,6 +266,11 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
263266
}
264267
ctrlOptions.Log = ctrlOptions.Log.WithValues("reconcilerGroup", gvk.Group, "reconcilerKind", gvk.Kind)
265268

269+
if blder.forInput.conditionallyRun {
270+
ctrlOptions.ConditionalOn = &blder.forInput.object
271+
ctrlOptions.ConditionalWaitTime = blder.forInput.waitTime
272+
}
273+
266274
// Build the controller and return.
267275
blder.ctrl, err = newController(blder.getControllerName(gvk), blder.mgr, ctrlOptions)
268276
return err

pkg/builder/options.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package builder
1818

1919
import (
20+
"time"
21+
2022
"sigs.k8s.io/controller-runtime/pkg/predicate"
2123
)
2224

@@ -75,4 +77,21 @@ var _ ForOption = &Predicates{}
7577
var _ OwnsOption = &Predicates{}
7678
var _ WatchesOption = &Predicates{}
7779

80+
// ConditionallyRun runs the controller
81+
// condtionally on the existence of the forInput object
82+
// in the cluster's discovery doc, letting you start a
83+
// controller manager for a CRD not yet installed on the cluster.
84+
type ConditionallyRun struct {
85+
waitTime time.Duration
86+
}
87+
88+
func (w ConditionallyRun) ApplyToFor(opts *ForInput) {
89+
opts.conditionallyRun = true
90+
if w.waitTime == time.Duration(0) {
91+
opts.waitTime = 100 * time.Microsecond
92+
} else {
93+
opts.waitTime = w.waitTime
94+
}
95+
}
96+
7897
// }}}

pkg/cache/cache.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type Informers interface {
5858
// of the underlying object.
5959
GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (Informer, error)
6060

61+
// Remove the informer for the given object.
62+
Remove(obj runtime.Object) error
63+
6164
// Start runs all the informers known to this cache until the given channel is closed.
6265
// It blocks.
6366
Start(stopCh <-chan struct{}) error

pkg/cache/informertest/fake_cache.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ func (c *FakeInformers) Start(stopCh <-chan struct{}) error {
125125
return c.Error
126126
}
127127

128+
// Remove implements Cache
129+
func (c *FakeInformers) Remove(obj runtime.Object) error {
130+
return c.Error
131+
}
132+
128133
// IndexField implements Cache
129134
func (c *FakeInformers) IndexField(ctx context.Context, obj runtime.Object, field string, extractValue client.IndexerFunc) error {
130135
return nil

pkg/cache/multi_namespace_cache.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ func (c *multiNamespaceCache) GetInformerForKind(ctx context.Context, gvk schema
9494
return &multiNamespaceInformer{namespaceToInformer: informers}, nil
9595
}
9696

97+
func (c *multiNamespaceCache) Remove(obj runtime.Object) error {
98+
for _, cache := range c.namespaceToCache {
99+
if err := cache.Remove(obj); err != nil {
100+
return err
101+
}
102+
}
103+
return nil
104+
}
105+
97106
func (c *multiNamespaceCache) Start(stopCh <-chan struct{}) error {
98107
for ns, cache := range c.namespaceToCache {
99108
go func(ns string, cache Cache) {

pkg/controller/controller.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package controller
1818

1919
import (
2020
"fmt"
21+
"time"
2122

2223
"github.com/go-logr/logr"
24+
"k8s.io/apimachinery/pkg/runtime"
2325
"k8s.io/client-go/util/workqueue"
2426
"sigs.k8s.io/controller-runtime/pkg/handler"
2527
"sigs.k8s.io/controller-runtime/pkg/internal/controller"
@@ -45,6 +47,20 @@ type Options struct {
4547

4648
// Log is the logger used for this controller.
4749
Log logr.Logger
50+
51+
// Conditionally is a flag set during controller setup that indicates it should conditionally wait on the
52+
// ForObject to appear in the discovery doc before starting the controller. If set,
53+
// it will set ConditionalObject to the ForObject being reconciled for in the controller builder.
54+
//Conditionally bool
55+
56+
// ConditionalObject is the Object that the manager should wait on to appear
57+
// in the discovery document to indicate to begin running the controller
58+
// (and should stop the informer for this object if it disappears from the discovery doc).
59+
ConditionalOn *runtime.Object
60+
61+
// ConditionalWaitTime is the frequency at which the controller manager should check
62+
// the discovery doc for the existence of the CondtionalOn object.
63+
ConditionalWaitTime time.Duration
4864
}
4965

5066
// Controller implements a Kubernetes API. A Controller manages a work queue fed reconcile.Requests
@@ -117,6 +133,8 @@ func NewUnmanaged(name string, mgr manager.Manager, options Options) (Controller
117133
MaxConcurrentReconciles: options.MaxConcurrentReconciles,
118134
SetFields: mgr.SetFields,
119135
Name: name,
136+
ConditionalOn: options.ConditionalOn,
137+
ConditionalWaitTime: options.ConditionalWaitTime,
120138
Log: options.Log.WithName("controller").WithValues("controller", name),
121139
}, nil
122140
}

pkg/internal/controller/controller.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"time"
2323

2424
"github.com/go-logr/logr"
25+
"k8s.io/apimachinery/pkg/runtime"
2526
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2627
"k8s.io/apimachinery/pkg/util/wait"
2728
"k8s.io/client-go/util/workqueue"
@@ -43,6 +44,20 @@ type Controller struct {
4344
// MaxConcurrentReconciles is the maximum number of concurrent Reconciles which can be run. Defaults to 1.
4445
MaxConcurrentReconciles int
4546

47+
// Conditionally is a flag set during controller setup that indicates it should conditionally wait on the
48+
// ForObject to appear in the discovery doc before starting the controller. If set,
49+
// it will set ConditionalObject to the ForObject being reconciled for in the controller builder.
50+
// Conditionally bool
51+
52+
// ConditionalOn is the Object that the manager should wait on to appear
53+
// in the discovery document to indicate to begin running the controller
54+
// (and should stop the informer for this object if it disappears from the discovery doc).
55+
ConditionalOn *runtime.Object
56+
57+
// ConditionalWaitTime is the frequency at which the controller manager should check
58+
// the discovery doc for the existence of the CondtionalOn object.
59+
ConditionalWaitTime time.Duration
60+
4661
// Reconciler is a function that can be called at any time with the Name / Namespace of an object and
4762
// ensures that the state of the system matches the state specified in the object.
4863
// Defaults to the DefaultReconcileFunc.
@@ -182,6 +197,20 @@ func (c *Controller) Start(stop <-chan struct{}) error {
182197
return nil
183198
}
184199

200+
// GetConditionalOn returns the object that startCondtionalRunnables used
201+
// to determine whether a condtional runnable should be started/stopped (based
202+
// on the object's existence in the cluster's discovery doc).
203+
func (c *Controller) GetConditionalOn() *runtime.Object {
204+
return c.ConditionalOn
205+
}
206+
207+
// GetConditionalWaitTime returns the duration for which the
208+
// controller manager should wait on each iteration when checking the
209+
// discovery doc for the ConditionalOn object's existence.
210+
func (c *Controller) GetConditionalWaitTime() time.Duration {
211+
return c.ConditionalWaitTime
212+
}
213+
185214
// worker runs a worker thread that just dequeues items, processes them, and marks them done.
186215
// It enforces that the reconcileHandler is never invoked concurrently with the same object.
187216
func (c *Controller) worker() {

0 commit comments

Comments
 (0)