Skip to content

Commit da7a60e

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 9bc967a commit da7a60e

27 files changed

+621
-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.

alias.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
)
3131

3232
// Builder builds an Application ControllerManagedBy (e.g. Operator) and returns a manager.Manager to start it.
33-
type Builder = builder.Builder
33+
type Builder = builder.Builder[reconcile.Request]
3434

3535
// Request contains the information necessary to reconcile a Kubernetes object. This includes the
3636
// information to uniquely identify the object - its Name and Namespace. It does NOT contain information about

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: 65 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,27 @@ const (
5252
)
5353

5454
// Builder builds a Controller.
55-
type Builder struct {
55+
type Builder[request comparable] struct {
5656
forInput ForInput
5757
ownsInput []OwnsInput
58-
rawSources []source.Source
59-
watchesInput []WatchesInput
58+
rawSources []source.TypedSource[request]
59+
watchesInput []WatchesInput[request]
6060
mgr manager.Manager
6161
globalPredicates []predicate.Predicate
62-
ctrl controller.Controller
63-
ctrlOptions controller.Options
62+
ctrl controller.TypedController[request]
63+
ctrlOptions controller.TypedOptions[request]
6464
name string
65+
newController func(name string, mgr manager.Manager, options controller.TypedOptions[request]) (controller.TypedController[request], error)
6566
}
6667

6768
// 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}
69+
func ControllerManagedBy(m manager.Manager) *Builder[reconcile.Request] {
70+
return TypedControllerManagedBy[reconcile.Request](m)
71+
}
72+
73+
// TypedControllerManagedBy returns a new tyepd controller builder that will be started by the provided Manager.
74+
func TypedControllerManagedBy[request comparable](m manager.Manager) *Builder[request] {
75+
return &Builder[request]{mgr: m}
7076
}
7177

7278
// ForInput represents the information set by the For method.
@@ -81,7 +87,7 @@ type ForInput struct {
8187
// update events by *reconciling the object*.
8288
// This is the equivalent of calling
8389
// Watches(&source.Kind{Type: apiType}, &handler.EnqueueRequestForObject{}).
84-
func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
90+
func (blder *Builder[request]) For(object client.Object, opts ...ForOption) *Builder[request] {
8591
if blder.forInput.object != nil {
8692
blder.forInput.err = fmt.Errorf("For(...) should only be called once, could not assign multiple objects for reconciliation")
8793
return blder
@@ -111,7 +117,7 @@ type OwnsInput struct {
111117
//
112118
// By default, this is the equivalent of calling
113119
// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())).
114-
func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
120+
func (blder *Builder[request]) Owns(object client.Object, opts ...OwnsOption) *Builder[request] {
115121
input := OwnsInput{object: object}
116122
for _, opt := range opts {
117123
opt.ApplyToOwns(&input)
@@ -122,9 +128,9 @@ func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
122128
}
123129

124130
// WatchesInput represents the information set by Watches method.
125-
type WatchesInput struct {
131+
type WatchesInput[request comparable] struct {
126132
obj client.Object
127-
handler handler.EventHandler
133+
handler handler.TypedEventHandler[client.Object, request]
128134
predicates []predicate.Predicate
129135
objectProjection objectProjection
130136
}
@@ -134,8 +140,12 @@ type WatchesInput struct {
134140
//
135141
// This is the equivalent of calling
136142
// WatchesRawSource(source.Kind(cache, object, eventHandler, predicates...)).
137-
func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
138-
input := WatchesInput{
143+
func (blder *Builder[request]) Watches(
144+
object client.Object,
145+
eventHandler handler.TypedEventHandler[client.Object, request],
146+
opts ...WatchesOption[request],
147+
) *Builder[request] {
148+
input := WatchesInput[request]{
139149
obj: object,
140150
handler: eventHandler,
141151
}
@@ -175,8 +185,12 @@ func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHa
175185
// In the first case, controller-runtime will create another cache for the
176186
// concrete type on top of the metadata cache; this increases memory
177187
// 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)
188+
func (blder *Builder[request]) WatchesMetadata(
189+
object client.Object,
190+
eventHandler handler.TypedEventHandler[client.Object, request],
191+
opts ...WatchesOption[request],
192+
) *Builder[request] {
193+
opts = append(opts, projectAs[request](projectAsMetadata))
180194
return blder.Watches(object, eventHandler, opts...)
181195
}
182196

@@ -187,7 +201,7 @@ func (blder *Builder) WatchesMetadata(object client.Object, eventHandler handler
187201
// This method is only exposed for more advanced use cases, most users should use one of the higher level functions.
188202
//
189203
// WatchesRawSource does not respect predicates configured through WithEventFilter.
190-
func (blder *Builder) WatchesRawSource(src source.Source) *Builder {
204+
func (blder *Builder[request]) WatchesRawSource(src source.TypedSource[request]) *Builder[request] {
191205
blder.rawSources = append(blder.rawSources, src)
192206

193207
return blder
@@ -197,19 +211,19 @@ func (blder *Builder) WatchesRawSource(src source.Source) *Builder {
197211
// trigger reconciliations. For example, filtering on whether the resource version has changed.
198212
// Given predicate is added for all watched objects.
199213
// Defaults to the empty list.
200-
func (blder *Builder) WithEventFilter(p predicate.Predicate) *Builder {
214+
func (blder *Builder[request]) WithEventFilter(p predicate.Predicate) *Builder[request] {
201215
blder.globalPredicates = append(blder.globalPredicates, p)
202216
return blder
203217
}
204218

205219
// WithOptions overrides the controller options used in doController. Defaults to empty.
206-
func (blder *Builder) WithOptions(options controller.Options) *Builder {
220+
func (blder *Builder[request]) WithOptions(options controller.TypedOptions[request]) *Builder[request] {
207221
blder.ctrlOptions = options
208222
return blder
209223
}
210224

211225
// WithLogConstructor overrides the controller options's LogConstructor.
212-
func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request) logr.Logger) *Builder {
226+
func (blder *Builder[request]) WithLogConstructor(logConstructor func(*request) logr.Logger) *Builder[request] {
213227
blder.ctrlOptions.LogConstructor = logConstructor
214228
return blder
215229
}
@@ -219,19 +233,19 @@ func (blder *Builder) WithLogConstructor(logConstructor func(*reconcile.Request)
219233
// (underscores and alphanumeric characters only).
220234
//
221235
// By default, controllers are named using the lowercase version of their kind.
222-
func (blder *Builder) Named(name string) *Builder {
236+
func (blder *Builder[request]) Named(name string) *Builder[request] {
223237
blder.name = name
224238
return blder
225239
}
226240

227241
// Complete builds the Application Controller.
228-
func (blder *Builder) Complete(r reconcile.Reconciler) error {
242+
func (blder *Builder[request]) Complete(r reconcile.TypedReconciler[request]) error {
229243
_, err := blder.Build(r)
230244
return err
231245
}
232246

233247
// Build builds the Application Controller and returns the Controller it created.
234-
func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, error) {
248+
func (blder *Builder[request]) Build(r reconcile.TypedReconciler[request]) (controller.TypedController[request], error) {
235249
if r == nil {
236250
return nil, fmt.Errorf("must provide a non-nil Reconciler")
237251
}
@@ -255,7 +269,7 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
255269
return blder.ctrl, nil
256270
}
257271

258-
func (blder *Builder) project(obj client.Object, proj objectProjection) (client.Object, error) {
272+
func (blder *Builder[request]) project(obj client.Object, proj objectProjection) (client.Object, error) {
259273
switch proj {
260274
case projectAsNormal:
261275
return obj, nil
@@ -272,17 +286,23 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client.
272286
}
273287
}
274288

275-
func (blder *Builder) doWatch() error {
289+
func (blder *Builder[request]) doWatch() error {
276290
// Reconcile type
277291
if blder.forInput.object != nil {
278292
obj, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
279293
if err != nil {
280294
return err
281295
}
282-
hdler := &handler.EnqueueRequestForObject{}
296+
297+
var hdler handler.TypedEventHandler[client.Object, request]
298+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
299+
return errors.New("For() is not supported for TypedBuilder")
300+
}
301+
302+
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(&handler.EnqueueRequestForObject{}))
283303
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
284304
allPredicates = append(allPredicates, blder.forInput.predicates...)
285-
src := source.Kind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
305+
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
286306
if err := blder.ctrl.Watch(src); err != nil {
287307
return err
288308
}
@@ -301,14 +321,18 @@ func (blder *Builder) doWatch() error {
301321
if !own.matchEveryOwner {
302322
opts = append(opts, handler.OnlyControllerOwner())
303323
}
304-
hdler := handler.EnqueueRequestForOwner(
324+
var hdler handler.TypedEventHandler[client.Object, request]
325+
if reflect.TypeFor[request]() != reflect.TypeOf(reconcile.Request{}) {
326+
return errors.New("Owns() is not supported for TypedBuilder")
327+
}
328+
reflect.ValueOf(&hdler).Elem().Set(reflect.ValueOf(handler.EnqueueRequestForOwner(
305329
blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(),
306330
blder.forInput.object,
307331
opts...,
308-
)
332+
)))
309333
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
310334
allPredicates = append(allPredicates, own.predicates...)
311-
src := source.Kind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
335+
src := source.TypedKind(blder.mgr.GetCache(), obj, hdler, allPredicates...)
312336
if err := blder.ctrl.Watch(src); err != nil {
313337
return err
314338
}
@@ -325,7 +349,7 @@ func (blder *Builder) doWatch() error {
325349
}
326350
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
327351
allPredicates = append(allPredicates, w.predicates...)
328-
if err := blder.ctrl.Watch(source.Kind(blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
352+
if err := blder.ctrl.Watch(source.TypedKind[client.Object, request](blder.mgr.GetCache(), projected, w.handler, allPredicates...)); err != nil {
329353
return err
330354
}
331355
}
@@ -337,7 +361,7 @@ func (blder *Builder) doWatch() error {
337361
return nil
338362
}
339363

340-
func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
364+
func (blder *Builder[request]) getControllerName(gvk schema.GroupVersionKind, hasGVK bool) (string, error) {
341365
if blder.name != "" {
342366
return blder.name, nil
343367
}
@@ -347,7 +371,7 @@ func (blder *Builder) getControllerName(gvk schema.GroupVersionKind, hasGVK bool
347371
return strings.ToLower(gvk.Kind), nil
348372
}
349373

350-
func (blder *Builder) doController(r reconcile.Reconciler) error {
374+
func (blder *Builder[request]) doController(r reconcile.TypedReconciler[request]) error {
351375
globalOpts := blder.mgr.GetControllerOptions()
352376

353377
ctrlOptions := blder.ctrlOptions
@@ -401,9 +425,10 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
401425
)
402426
}
403427

404-
ctrlOptions.LogConstructor = func(req *reconcile.Request) logr.Logger {
428+
ctrlOptions.LogConstructor = func(in *request) logr.Logger {
405429
log := log
406-
if req != nil {
430+
431+
if req, ok := any(in).(*reconcile.Request); ok && req != nil {
407432
if hasGVK {
408433
log = log.WithValues(gvk.Kind, klog.KRef(req.Namespace, req.Name))
409434
}
@@ -415,7 +440,11 @@ func (blder *Builder) doController(r reconcile.Reconciler) error {
415440
}
416441
}
417442

443+
if blder.newController == nil {
444+
blder.newController = controller.NewTyped[request]
445+
}
446+
418447
// Build the controller and return.
419-
blder.ctrl, err = newController(controllerName, blder.mgr, ctrlOptions)
448+
blder.ctrl, err = blder.newController(controllerName, blder.mgr, ctrlOptions)
420449
return err
421450
}

0 commit comments

Comments
 (0)