Skip to content

Commit 2b0b941

Browse files
committed
⚠️ Add TypedReconciler
This change adds a TypedReconciler which allows to customize the type being used in the workqueue. There is a number of situations where a custom type might be better than the default `reconcile.Request`: * Multi-Cluster controllers might want to put the clusters in there * Some controllers do not reconcile individual resources of a given type but all of them at once, for example IngressControllers might do this * Some controllers do not operate on Kubernetes resources at all
1 parent 162a113 commit 2b0b941

28 files changed

+718
-567
lines changed

.golangci.yml

-4
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,6 @@ issues:
122122
- linters:
123123
- staticcheck
124124
text: "SA1019: .*The component config package has been deprecated and will be removed in a future release."
125-
- linters:
126-
- staticcheck
127-
# Will be addressed separately.
128-
text: "SA1019: workqueue.(RateLimitingInterface|DefaultControllerRateLimiter|New|NewItemExponentialFailureRateLimiter|NewRateLimitingQueueWithConfig|DefaultItemBasedRateLimiter|RateLimitingQueueConfig) is deprecated:"
129125
# With Go 1.16, the new embed directive can be used with an un-named import,
130126
# revive (previously, golint) only allows these to be imported in a main.go, which wouldn't work for us.
131127
# This directive allows the embed package to be imported with an underscore everywhere.

examples/typed/main.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
networkingv1 "k8s.io/api/networking/v1"
9+
ctrl "sigs.k8s.io/controller-runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/builder"
11+
"sigs.k8s.io/controller-runtime/pkg/handler"
12+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
13+
"sigs.k8s.io/controller-runtime/pkg/source"
14+
)
15+
16+
func main() {
17+
if err := run(); err != nil {
18+
fmt.Fprintf(os.Stderr, "%v\n", err)
19+
os.Exit(1)
20+
}
21+
}
22+
23+
func run() error {
24+
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{})
25+
if err != nil {
26+
return fmt.Errorf("failed to construct manager: %w", err)
27+
}
28+
29+
// Use a request type that is always equal to itself so the workqueue
30+
// de-duplicates all events.
31+
// This can for example be useful for an ingress-controller that
32+
// generates a config from all ingresses, rather than individual ones.
33+
type request struct{}
34+
35+
r := reconcile.TypedFunc[request](func(ctx context.Context, _ request) (reconcile.Result, error) {
36+
ingressList := &networkingv1.IngressList{}
37+
if err := mgr.GetClient().List(ctx, ingressList); err != nil {
38+
return reconcile.Result{}, fmt.Errorf("failed to list ingresses: %w", err)
39+
}
40+
41+
buildIngressConfig(ingressList)
42+
return reconcile.Result{}, nil
43+
})
44+
if err := builder.TypedControllerManagedBy[request](mgr).
45+
WatchesRawSource(source.TypedKind(
46+
mgr.GetCache(),
47+
&networkingv1.Ingress{},
48+
handler.TypedEnqueueRequestsFromMapFunc(func(context.Context, *networkingv1.Ingress) []request {
49+
return []request{{}}
50+
})),
51+
).
52+
Named("ingress_controller").
53+
Complete(r); err != nil {
54+
return fmt.Errorf("failed to construct ingress-controller: %w", err)
55+
}
56+
57+
return nil
58+
}
59+
60+
func buildIngressConfig(*networkingv1.IngressList) {}

pkg/builder/controller.go

+71-35
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package builder
1919
import (
2020
"errors"
2121
"fmt"
22+
"reflect"
2223
"strings"
2324

2425
"github.com/go-logr/logr"
@@ -37,7 +38,6 @@ import (
3738
)
3839

3940
// Supporting mocking out functions for testing.
40-
var newController = controller.New
4141
var getGvk = apiutil.GVKForObject
4242

4343
// project represents other forms that we can use to
@@ -52,21 +52,32 @@ const (
5252
)
5353

5454
// Builder builds a Controller.
55-
type Builder struct {
55+
type Builder = TypedBuilder[reconcile.Request]
56+
57+
// TypedBuilder builds a Controller. The request is the request type
58+
// that is passed to the workqueue and then to the Reconciler.
59+
// The workqueue de-duplicates identical requests.
60+
type TypedBuilder[request comparable] struct {
5661
forInput ForInput
5762
ownsInput []OwnsInput
58-
rawSources []source.Source
59-
watchesInput []WatchesInput
63+
rawSources []source.TypedSource[request]
64+
watchesInput []WatchesInput[request]
6065
mgr manager.Manager
6166
globalPredicates []predicate.Predicate
62-
ctrl controller.Controller
63-
ctrlOptions controller.Options
67+
ctrl controller.TypedController[request]
68+
ctrlOptions controller.TypedOptions[request]
6469
name string
70+
newController func(name string, mgr manager.Manager, options controller.TypedOptions[request]) (controller.TypedController[request], error)
6571
}
6672

6773
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager.
6874
func ControllerManagedBy(m manager.Manager) *Builder {
69-
return &Builder{mgr: m}
75+
return TypedControllerManagedBy[reconcile.Request](m)
76+
}
77+
78+
// TypedControllerManagedBy returns a new typed controller builder that will be started by the provided Manager.
79+
func TypedControllerManagedBy[request comparable](m manager.Manager) *TypedBuilder[request] {
80+
return &TypedBuilder[request]{mgr: m}
7081
}
7182

7283
// ForInput represents the information set by the For method.
@@ -81,7 +92,7 @@ type ForInput struct {
8192
// update events by *reconciling the object*.
8293
// This is the equivalent of calling
8394
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}).
84-
func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
95+
func (blder *TypedBuilder[request]) For(object client.Object, opts ...ForOption) *TypedBuilder[request] {
8596
if blder.forInput.object != nil {
8697
blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation")
8798
return blder
@@ -111,7 +122,7 @@ type OwnsInput struct {
111122
//
112123
// By default, this is the equivalent of calling
113124
// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())).
114-
func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
125+
func (blder *TypedBuilder[request]) Owns(object client.Object, opts ...OwnsOption) *TypedBuilder[request] {
115126
input := OwnsInput{object: object}
116127
for _, opt := range opts {
117128
opt.ApplyToOwns(&input)
@@ -122,9 +133,9 @@ func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
122133
}
123134

124135
// WatchesInput represents the information set by Watches method.
125-
type WatchesInput struct {
136+
type WatchesInput[request comparable] struct {
126137
obj client.Object
127-
handler handler.EventHandler
138+
handler handler.TypedEventHandler[client.Object, request]
128139
predicates []predicate.Predicate
129140
objectProjection objectProjection
130141
}
@@ -134,8 +145,12 @@ type WatchesInput struct {
134145
//
135146
// This is the equivalent of calling
136147
// WatchesRawSource(source.Kind(cache, object, eventHandler, predicates...)).
137-
func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
138-
input := WatchesInput{
148+
func (blder *TypedBuilder[request]) Watches(
149+
object client.Object,
150+
eventHandler handler.TypedEventHandler[client.Object, request],
151+
opts ...WatchesOption[request],
152+
) *TypedBuilder[request] {
153+
input := WatchesInput[request]{
139154
obj: object,
140155
handler: eventHandler,
141156
}
@@ -175,8 +190,12 @@ func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHa
175190
// In the first case, controller-runtime will create another cache for the
176191
// concrete type on top of the metadata cache; this increases memory
177192
// consumption and leads to race conditions as caches are not in sync.
178-
func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
179-
opts = append(opts, OnlyMetadata)
193+
func (blder *TypedBuilder[request]) WatchesMetadata(
194+
object client.Object,
195+
eventHandler handler.TypedEventHandler[client.Object, request],
196+
opts ...WatchesOption[request],
197+
) *TypedBuilder[request] {
198+
opts = append(opts, projectAs[request](projectAsMetadata))
180199
return blder.Watches(object, eventHandler, opts...)
181200
}
182201

@@ -187,7 +206,7 @@ func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler
187206
// This method is only exposed for more advanced use cases, most users should use one of the higher level functions.
188207
//
189208
// WatchesRawSource does not respect predicates configured through WithEventFilter.
190-
func (blder *Builder) WatchesRawSource(src source.Source) *Builder {
209+
func (blder *TypedBuilder[request]) WatchesRawSource(src source.TypedSource[request]) *TypedBuilder[request] {
191210
blder.rawSources = append(blder.rawSources, src)
192211

193212
return blder
@@ -197,19 +216,19 @@ func (blder *Builder) WatchesRawSource(src source.Source) *Builder {
197216
// trigger reconciliations. For example, filtering on whether the resource version has changed.
198217
// Given predicate is added for all watched objects.
199218
// Defaults to the empty list.
200-
func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
219+
func (blder *TypedBuilder[request]) WithEventFilter(p predicate.Predicate) *TypedBuilder[request] {
201220
blder.globalPredicates = append(blder.globalPredicates, p)
202221
return blder
203222
}
204223

205224
// WithOptions overrides the controller options used in doController. Defaults to empty.
206-
func (blder *Builder) WithOptions(options controller.Options) *Builder {
225+
func (blder *TypedBuilder[request]) WithOptions(options controller.TypedOptions[request]) *TypedBuilder[request] {
207226
blder.ctrlOptions = options
208227
return blder
209228
}
210229

211230
// WithLogConstructor overrides the controller options's LogConstructor.
212-
func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request) logr.Logger) *Builder {
231+
func (blder *TypedBuilder[request]) WithLogConstructor(logConstructor func(*request) logr.Logger) *TypedBuilder[request] {
213232
blder.ctrlOptions.LogConstructor = logConstructor
214233
return blder
215234
}
@@ -219,19 +238,19 @@ func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request)
219238
// (underscores and alphanumeric characters only).
220239
//
221240
// By default, controllers are named using the lowercase version of their kind.
222-
func (blder *Builder) Named(name string) *Builder {
241+
func (blder *TypedBuilder[request]) Named(name string) *TypedBuilder[request] {
223242
blder.name = name
224243
return blder
225244
}
226245

227246
// Complete builds the Application Controller.
228-
func (blder *Builder) Complete(r reconcile.Reconciler) error {
247+
func (blder *TypedBuilder[request]) Complete(r reconcile.TypedReconciler[request]) error {
229248
_, err := blder.Build(r)
230249
return err
231250
}
232251

233252
// Build builds the Application Controller and returns the Controller it created.
234-
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
253+
func (blder *TypedBuilder[request]) Build(r reconcile.TypedReconciler[request]) (controller.TypedController[request], error) {
235254
if r == nil {
236255
return nil, fmt.Errorf("must provide a non-nil Reconciler")
237256
}
@@ -255,7 +274,7 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
255274
return blder.ctrl, nil
256275
}
257276

258-
func (blder *Builder) project(obj client.Object, proj objectProjection) (client.Object, error) {
277+
func (blder *TypedBuilder[request]) project(obj client.Object, proj objectProjection) (client.Object, error) {
259278
switch proj {
260279
case projectAsNormal:
261280
return obj, nil
@@ -272,17 +291,23 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client.
272291
}
273292
}
274293

275-
func (blder *Builder) doWatch() error {
294+
func (blder *TypedBuilder[request]) doWatch() error {
276295
// Reconcile type
277296
if blder.forInput.object != nil {
278297
obj, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
279298
if err != nil {
280299
return err
281300
}
282-
hdler := &handler.EnqueueRequestForObject{}
301+
302+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
303+
return fmt.Errorf("For() can only be used with reconcile.Request, got %T", reflect.TypeFor[request]())
304+
}
305+
306+
var hdler handler.TypedEventHandler[client.Object, request]
307+
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(&handler.EnqueueRequestForObject{}))
283308
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
284309
allPredicates = append(allPredicates, blder.forInput.predicates...)
285-
src := source.Kind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
310+
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
286311
if err := blder.ctrl.Watch(src); err != nil {
287312
return err
288313
}
@@ -301,14 +326,20 @@ func (blder *Builder) doWatch() error {
301326
if !own.matchEveryOwner {
302327
opts = append(opts, handler.OnlyControllerOwner())
303328
}
304-
hdler := handler.EnqueueRequestForOwner(
329+
330+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
331+
return errors.New("Owns() is not supported for TypedBuilder")
332+
}
333+
334+
var hdler handler.TypedEventHandler[client.Object, request]
335+
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(handler.EnqueueRequestForOwner(
305336
blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(),
306337
blder.forInput.object,
307338
opts...,
308-
)
339+
)))
309340
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
310341
allPredicates = append(allPredicates, own.predicates...)
311-
src := source.Kind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
342+
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
312343
if err := blder.ctrl.Watch(src); err != nil {
313344
return err
314345
}
@@ -325,7 +356,7 @@ func (blder *Builder) doWatch() error {
325356
}
326357
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
327358
allPredicates = append(allPredicates, w.predicates...)
328-
if err := blder.ctrl.Watch(source.Kind(blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
359+
if err := blder.ctrl.Watch(source.TypedKind[client.Object, request](blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
329360
return err
330361
}
331362
}
@@ -337,7 +368,7 @@ func (blder *Builder) doWatch() error {
337368
return nil
338369
}
339370

340-
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
371+
func (blder *TypedBuilder[request]) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
341372
if blder.name != "" {
342373
return blder.name, nil
343374
}
@@ -347,7 +378,7 @@ func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool
347378
return strings.ToLower(gvk.Kind), nil
348379
}
349380

350-
func (blder *Builder) doController(r reconcile.Reconciler) error {
381+
func (blder *TypedBuilder[request]) doController(r reconcile.TypedReconciler[request]) error {
351382
globalOpts := blder.mgr.GetControllerOptions()
352383

353384
ctrlOptions := blder.ctrlOptions
@@ -401,9 +432,10 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
401432
)
402433
}
403434

404-
ctrlOptions.LogConstructor = func(req *reconcile.Request) logr.Logger {
435+
ctrlOptions.LogConstructor = func(in *request) logr.Logger {
405436
log := log
406-
if req != nil {
437+
438+
if req, ok := any(in).(*reconcile.Request); ok && req != nil {
407439
if hasGVK {
408440
log = log.WithValues(gvk.Kind, klog.KRef(req.Namespace, req.Name))
409441
}
@@ -415,7 +447,11 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
415447
}
416448
}
417449

450+
if blder.newController == nil {
451+
blder.newController = controller.NewTyped[request]
452+
}
453+
418454
// Build the controller and return.
419-
blder.ctrl, err = newController(controllerName, blder.mgr, ctrlOptions)
455+
blder.ctrl, err = blder.newController(controllerName, blder.mgr, ctrlOptions)
420456
return err
421457
}

0 commit comments

Comments
 (0)