Skip to content

Commit e8833b6

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 8290d13 commit e8833b6

26 files changed

+629
-489
lines changed

.golangci.yml

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
type request struct{}
30+
31+
r := reconcile.TypedFunc[request](func(ctx context.Context, _ request) (reconcile.Result, error) {
32+
ingressList := &networkingv1.IngressList{}
33+
if err := mgr.GetClient().List(ctx, ingressList); err != nil {
34+
return reconcile.Result{}, fmt.Errorf("failed to list ingresses: %w", err)
35+
}
36+
37+
buildIngressConfig(ingressList)
38+
return reconcile.Result{}, nil
39+
})
40+
if err := builder.TypedControllerManagedBy[request](mgr).
41+
WatchesRawSource(source.TypedKind(
42+
mgr.GetCache(),
43+
&networkingv1.Ingress{},
44+
handler.TypedEnqueueRequestsFromMapFunc(func(context.Context, *networkingv1.Ingress) []request {
45+
return []request{{}}
46+
})),
47+
).
48+
Named("ingress_controller").
49+
Complete(r); err != nil {
50+
return fmt.Errorf("failed to construct ingress-controller: %w", err)
51+
}
52+
53+
return nil
54+
}
55+
56+
func buildIngressConfig(*networkingv1.IngressList) {}

pkg/builder/controller.go

Lines changed: 70 additions & 36 deletions
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.
68-
func ControllerManagedBy(m manager.Manager) *Builder {
69-
return &Builder{mgr: m}
74+
func ControllerManagedBy(m manager.Manager) *TypedBuilder[reconcile.Request] {
75+
return TypedControllerManagedBy[reconcile.Request](m)
76+
}
77+
78+
// TypedControllerManagedBy returns a new tyepd 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+
var hdler handler.TypedEventHandler[client.Object, request]
303+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
304+
return errors.New("For() is not supported for TypedBuilder")
305+
}
306+
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,18 @@ func (blder *Builder) doWatch() error {
301326
if !own.matchEveryOwner {
302327
opts = append(opts, handler.OnlyControllerOwner())
303328
}
304-
hdler := handler.EnqueueRequestForOwner(
329+
var hdler handler.TypedEventHandler[client.Object, request]
330+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
331+
return errors.New("Owns() is not supported for TypedBuilder")
332+
}
333+
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(handler.EnqueueRequestForOwner(
305334
blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(),
306335
blder.forInput.object,
307336
opts...,
308-
)
337+
)))
309338
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
310339
allPredicates = append(allPredicates, own.predicates...)
311-
src := source.Kind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
340+
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
312341
if err := blder.ctrl.Watch(src); err != nil {
313342
return err
314343
}
@@ -325,7 +354,7 @@ func (blder *Builder) doWatch() error {
325354
}
326355
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
327356
allPredicates = append(allPredicates, w.predicates...)
328-
if err := blder.ctrl.Watch(source.Kind(blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
357+
if err := blder.ctrl.Watch(source.TypedKind[client.Object, request](blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
329358
return err
330359
}
331360
}
@@ -337,7 +366,7 @@ func (blder *Builder) doWatch() error {
337366
return nil
338367
}
339368

340-
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
369+
func (blder *TypedBuilder[request]) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
341370
if blder.name != "" {
342371
return blder.name, nil
343372
}
@@ -347,7 +376,7 @@ func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool
347376
return strings.ToLower(gvk.Kind), nil
348377
}
349378

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

353382
ctrlOptions := blder.ctrlOptions
@@ -401,9 +430,10 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
401430
)
402431
}
403432

404-
ctrlOptions.LogConstructor = func(req *reconcile.Request) logr.Logger {
433+
ctrlOptions.LogConstructor = func(in *request) logr.Logger {
405434
log := log
406-
if req != nil {
435+
436+
if req, ok := any(in).(*reconcile.Request); ok && req != nil {
407437
if hasGVK {
408438
log = log.WithValues(gvk.Kind, klog.KRef(req.Namespace, req.Name))
409439
}
@@ -415,7 +445,11 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
415445
}
416446
}
417447

448+
if blder.newController == nil {
449+
blder.newController = controller.NewTyped[request]
450+
}
451+
418452
// Build the controller and return.
419-
blder.ctrl, err = newController(controllerName, blder.mgr, ctrlOptions)
453+
blder.ctrl, err = blder.newController(controllerName, blder.mgr, ctrlOptions)
420454
return err
421455
}

0 commit comments

Comments
 (0)