@@ -17,6 +17,7 @@ limitations under the License.
17
17
package builder
18
18
19
19
import (
20
+ "context"
20
21
"errors"
21
22
"fmt"
22
23
"reflect"
@@ -25,10 +26,12 @@ import (
25
26
"github.com/go-logr/logr"
26
27
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27
28
"k8s.io/apimachinery/pkg/runtime/schema"
29
+ "k8s.io/client-go/util/workqueue"
28
30
"k8s.io/klog/v2"
29
31
30
32
"sigs.k8s.io/controller-runtime/pkg/client"
31
33
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
34
+ "sigs.k8s.io/controller-runtime/pkg/cluster"
32
35
"sigs.k8s.io/controller-runtime/pkg/controller"
33
36
"sigs.k8s.io/controller-runtime/pkg/handler"
34
37
"sigs.k8s.io/controller-runtime/pkg/manager"
@@ -37,6 +40,9 @@ import (
37
40
"sigs.k8s.io/controller-runtime/pkg/source"
38
41
)
39
42
43
+ // Supporting mocking out functions for testing.
44
+ var getGvk = apiutil .GVKForObject
45
+
40
46
// project represents other forms that we can use to
41
47
// send/receive a given resource (metadata-only, unstructured, etc).
42
48
type objectProjection int
@@ -48,23 +54,30 @@ const (
48
54
projectAsMetadata
49
55
)
50
56
57
+ // clusterWatcher sets up watches between a cluster and a controller.
58
+ type typedClusterWatcher [request comparable ] struct {
59
+ ctrl controller.TypedController [request ]
60
+ forInput ForInput
61
+ ownsInput []OwnsInput
62
+ watchesInput []WatchesInput [request ]
63
+ globalPredicates []predicate.Predicate
64
+ clusterAwareRawSources []source.TypedClusterAwareSource [request ]
65
+ handlerWrapper func (handler.TypedEventHandler [client.Object , request ], cluster.Cluster ) handler.TypedEventHandler [client.Object , request ]
66
+ }
67
+
51
68
// Builder builds a Controller.
52
69
type Builder = TypedBuilder [reconcile.Request ]
53
70
54
71
// TypedBuilder builds a Controller. The request is the request type
55
72
// that is passed to the workqueue and then to the Reconciler.
56
73
// The workqueue de-duplicates identical requests.
57
74
type TypedBuilder [request comparable ] struct {
58
- forInput ForInput
59
- ownsInput []OwnsInput
60
- rawSources []source.TypedSource [request ]
61
- watchesInput []WatchesInput [request ]
62
- mgr manager.Manager
63
- globalPredicates []predicate.Predicate
64
- ctrl controller.TypedController [request ]
65
- ctrlOptions controller.TypedOptions [request ]
66
- name string
67
- newController func (name string , mgr manager.Manager , options controller.TypedOptions [request ]) (controller.TypedController [request ], error )
75
+ typedClusterWatcher [request ]
76
+ mgr manager.Manager
77
+ ctrlOptions controller.TypedOptions [request ]
78
+ name string
79
+ rawSources []source.TypedSource [request ]
80
+ newController func (name string , mgr manager.Manager , options controller.TypedOptions [request ]) (controller.TypedController [request ], error )
68
81
}
69
82
70
83
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager.
@@ -216,8 +229,12 @@ func (blder *TypedBuilder[request]) WatchesMetadata(
216
229
//
217
230
// WatchesRawSource makes it possible to use typed handlers and predicates with `source.Kind` as well as custom source implementations.
218
231
func (blder * TypedBuilder [request ]) WatchesRawSource (src source.TypedSource [request ]) * TypedBuilder [request ] {
219
- blder .rawSources = append (blder .rawSources , src )
232
+ if src , ok := src .(source.TypedClusterAwareSource [request ]); ok {
233
+ blder .clusterAwareRawSources = append (blder .clusterAwareRawSources , src )
234
+ return blder
235
+ }
220
236
237
+ blder .rawSources = append (blder .rawSources , src )
221
238
return blder
222
239
}
223
240
@@ -232,6 +249,12 @@ func (blder *TypedBuilder[request]) WithEventFilter(p predicate.Predicate) *Type
232
249
return blder
233
250
}
234
251
252
+ // WithHandlerWrapper sets an optional wrapper function that is applied to the TypedEventHandler for watches set up via the Watches func.
253
+ func (blder * TypedBuilder [request ]) WithHandlerWrapper (handlerWrapper func (handler.TypedEventHandler [client.Object , request ], cluster.Cluster ) handler.TypedEventHandler [client.Object , request ]) * TypedBuilder [request ] {
254
+ blder .handlerWrapper = handlerWrapper
255
+ return blder
256
+ }
257
+
235
258
// WithOptions overrides the controller options used in doController. Defaults to empty.
236
259
func (blder * TypedBuilder [request ]) WithOptions (options controller.TypedOptions [request ]) * TypedBuilder [request ] {
237
260
blder .ctrlOptions = options
@@ -279,35 +302,33 @@ func (blder *TypedBuilder[request]) Build(r reconcile.TypedReconciler[request])
279
302
return nil , err
280
303
}
281
304
305
+ if blder .ctrlOptions .EngageWithDefaultCluster == nil {
306
+ blder .ctrlOptions .EngageWithDefaultCluster = blder .mgr .GetControllerOptions ().EngageWithDefaultCluster
307
+ }
308
+
309
+ if blder .ctrlOptions .EngageWithProviderClusters == nil {
310
+ blder .ctrlOptions .EngageWithProviderClusters = blder .mgr .GetControllerOptions ().EngageWithProviderClusters
311
+ }
312
+
282
313
// Set the Watch
283
314
if err := blder .doWatch (); err != nil {
284
315
return nil , err
285
316
}
286
317
287
- return blder .ctrl , nil
288
- }
289
-
290
- func (blder * TypedBuilder [request ]) project (obj client.Object , proj objectProjection ) (client.Object , error ) {
291
- switch proj {
292
- case projectAsNormal :
293
- return obj , nil
294
- case projectAsMetadata :
295
- metaObj := & metav1.PartialObjectMetadata {}
296
- gvk , err := apiutil .GVKForObject (obj , blder .mgr .GetScheme ())
297
- if err != nil {
298
- return nil , fmt .Errorf ("unable to determine GVK of %T for a metadata-only watch: %w" , obj , err )
318
+ if * blder .ctrlOptions .EngageWithProviderClusters {
319
+ // wrap as cluster.Aware to be engaged with provider clusters on demand
320
+ if err := blder .mgr .Add (controller .NewTypedMultiClusterController (blder .ctrl , & blder .typedClusterWatcher )); err != nil {
321
+ return nil , err
299
322
}
300
- metaObj .SetGroupVersionKind (gvk )
301
- return metaObj , nil
302
- default :
303
- panic (fmt .Sprintf ("unexpected projection type %v on type %T, should not be possible since this is an internal field" , proj , obj ))
304
323
}
324
+
325
+ return blder .ctrl , nil
305
326
}
306
327
307
- func (blder * TypedBuilder [request ]) doWatch ( ) error {
328
+ func (cc * typedClusterWatcher [request ]) Watch ( ctx context. Context , cl cluster. Cluster ) error {
308
329
// Reconcile type
309
- if blder .forInput .object != nil {
310
- obj , err := blder . project (blder .forInput .object , blder .forInput .objectProjection )
330
+ if cc .forInput .object != nil {
331
+ obj , err := project (cl , cc .forInput .object , cc .forInput .objectProjection )
311
332
if err != nil {
312
333
return err
313
334
}
@@ -318,20 +339,16 @@ func (blder *TypedBuilder[request]) doWatch() error {
318
339
319
340
var hdler handler.TypedEventHandler [client.Object , request ]
320
341
reflect .ValueOf (& hdler ).Elem ().Set (reflect .ValueOf (& handler.EnqueueRequestForObject {}))
321
- allPredicates := append ([]predicate.Predicate (nil ), blder .globalPredicates ... )
322
- allPredicates = append (allPredicates , blder .forInput .predicates ... )
323
- src := source .TypedKind (blder . mgr . GetCache (), obj , hdler , allPredicates ... )
324
- if err := blder .ctrl .Watch (src ); err != nil {
342
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
343
+ allPredicates = append (allPredicates , cc .forInput .predicates ... )
344
+ src := & ctxBoundedSyncingSource [ request ]{ ctx : ctx , src : source .TypedKind (cl . GetCache (), obj , hdler , allPredicates ... )}
345
+ if err := cc .ctrl .Watch (src ); err != nil {
325
346
return err
326
347
}
327
348
}
328
349
329
- // Watches the managed types
330
- if len (blder .ownsInput ) > 0 && blder .forInput .object == nil {
331
- return errors .New ("Owns() can only be used together with For()" )
332
- }
333
- for _ , own := range blder .ownsInput {
334
- obj , err := blder .project (own .object , own .objectProjection )
350
+ for _ , own := range cc .ownsInput {
351
+ obj , err := project (cl , own .object , own .objectProjection )
335
352
if err != nil {
336
353
return err
337
354
}
@@ -342,37 +359,68 @@ func (blder *TypedBuilder[request]) doWatch() error {
342
359
343
360
var hdler handler.TypedEventHandler [client.Object , request ]
344
361
reflect .ValueOf (& hdler ).Elem ().Set (reflect .ValueOf (handler .EnqueueRequestForOwner (
345
- blder . mgr . GetScheme (), blder . mgr .GetRESTMapper (),
346
- blder .forInput .object ,
362
+ cl . GetScheme (), cl .GetRESTMapper (),
363
+ cc .forInput .object ,
347
364
opts ... ,
348
365
)))
349
- allPredicates := append ([]predicate.Predicate (nil ), blder .globalPredicates ... )
366
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
350
367
allPredicates = append (allPredicates , own .predicates ... )
351
- src := source .TypedKind (blder . mgr . GetCache (), obj , hdler , allPredicates ... )
352
- if err := blder .ctrl .Watch (src ); err != nil {
368
+ src := & ctxBoundedSyncingSource [ request ]{ ctx : ctx , src : source .TypedKind (cl . GetCache (), obj , hdler , allPredicates ... )}
369
+ if err := cc .ctrl .Watch (src ); err != nil {
353
370
return err
354
371
}
355
372
}
356
373
357
- // Do the watch requests
358
- if len (blder .watchesInput ) == 0 && blder .forInput .object == nil && len (blder .rawSources ) == 0 {
359
- return errors .New ("there are no watches configured, controller will never get triggered. Use For(), Owns(), Watches() or WatchesRawSource() to set them up" )
360
- }
361
- for _ , w := range blder .watchesInput {
362
- projected , err := blder .project (w .obj , w .objectProjection )
374
+ for _ , w := range cc .watchesInput {
375
+ projected , err := project (cl , w .obj , w .objectProjection )
363
376
if err != nil {
364
377
return fmt .Errorf ("failed to project for %T: %w" , w .obj , err )
365
378
}
366
- allPredicates := append ([]predicate.Predicate (nil ), blder .globalPredicates ... )
379
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
367
380
allPredicates = append (allPredicates , w .predicates ... )
368
- if err := blder .ctrl .Watch (source .TypedKind (blder .mgr .GetCache (), projected , w .handler , allPredicates ... )); err != nil {
381
+
382
+ handler := w .handler
383
+ if cc .handlerWrapper != nil {
384
+ handler = cc .handlerWrapper (handler , cl )
385
+ }
386
+
387
+ src := & ctxBoundedSyncingSource [request ]{ctx : ctx , src : source .TypedKind (cl .GetCache (), projected , handler , allPredicates ... )}
388
+ if err := cc .ctrl .Watch (src ); err != nil {
389
+ return err
390
+ }
391
+ }
392
+
393
+ for _ , src := range cc .clusterAwareRawSources {
394
+ if err := cc .ctrl .Watch (src ); err != nil {
369
395
return err
370
396
}
371
397
}
372
- for _ , src := range blder .rawSources {
373
- if err := blder .ctrl .Watch (src ); err != nil {
398
+
399
+ return nil
400
+ }
401
+
402
+ func (blder * TypedBuilder [request ]) doWatch () error {
403
+ // Pre-checks for a valid configuration
404
+ if len (blder .ownsInput ) > 0 && blder .forInput .object == nil {
405
+ return errors .New ("Owns() can only be used together with For()" )
406
+ }
407
+ if len (blder .watchesInput ) == 0 && blder .forInput .object == nil && len (blder .rawSources ) == 0 {
408
+ return errors .New ("there are no watches configured, controller will never get triggered. Use For(), Owns(), Watches() or WatchesRawSource() to set them up" )
409
+ }
410
+ if ! * blder .ctrlOptions .EngageWithDefaultCluster && len (blder .rawSources ) > 0 {
411
+ return errors .New ("when using a cluster adapter without watching the default cluster, non-cluster-aware custom raw watches are not allowed" )
412
+ }
413
+
414
+ if * blder .ctrlOptions .EngageWithDefaultCluster {
415
+ if err := blder .Watch (unboundedContext , blder .mgr ); err != nil {
374
416
return err
375
417
}
418
+
419
+ for _ , src := range blder .rawSources {
420
+ if err := blder .ctrl .Watch (src ); err != nil {
421
+ return err
422
+ }
423
+ }
376
424
}
377
425
return nil
378
426
}
@@ -464,3 +512,53 @@ func (blder *TypedBuilder[request]) doController(r reconcile.TypedReconciler[req
464
512
blder .ctrl , err = blder .newController (controllerName , blder .mgr , ctrlOptions )
465
513
return err
466
514
}
515
+
516
+ func project (cl cluster.Cluster , obj client.Object , proj objectProjection ) (client.Object , error ) {
517
+ switch proj {
518
+ case projectAsNormal :
519
+ return obj , nil
520
+ case projectAsMetadata :
521
+ metaObj := & metav1.PartialObjectMetadata {}
522
+ gvk , err := getGvk (obj , cl .GetScheme ())
523
+ if err != nil {
524
+ return nil , fmt .Errorf ("unable to determine GVK of %T for a metadata-only watch: %w" , obj , err )
525
+ }
526
+ metaObj .SetGroupVersionKind (gvk )
527
+ return metaObj , nil
528
+ default :
529
+ panic (fmt .Sprintf ("unexpected projection type %v on type %T, should not be possible since this is an internal field" , proj , obj ))
530
+ }
531
+ }
532
+
533
+ // ctxBoundedSyncingSource implements source.SyncingSource and wraps the ctx
534
+ // passed to the methods into the life-cycle of another context, i.e. stop
535
+ // whenever one of the contexts is done.
536
+ type ctxBoundedSyncingSource [request comparable ] struct {
537
+ ctx context.Context
538
+ src source.TypedSyncingSource [request ]
539
+ }
540
+
541
+ var unboundedContext context.Context = nil //nolint:revive // keep nil explicit for clarity.
542
+
543
+ var _ source.SyncingSource = & ctxBoundedSyncingSource [reconcile.Request ]{}
544
+
545
+ func (s * ctxBoundedSyncingSource [request ]) Start (ctx context.Context , q workqueue.TypedRateLimitingInterface [request ]) error {
546
+ return s .src .Start (joinContexts (ctx , s .ctx ), q )
547
+ }
548
+
549
+ func (s * ctxBoundedSyncingSource [request ]) WaitForSync (ctx context.Context ) error {
550
+ return s .src .WaitForSync (joinContexts (ctx , s .ctx ))
551
+ }
552
+
553
+ func joinContexts (ctx , bound context.Context ) context.Context {
554
+ if bound == unboundedContext {
555
+ return ctx
556
+ }
557
+
558
+ ctx , cancel := context .WithCancel (ctx )
559
+ go func () {
560
+ defer cancel ()
561
+ <- bound .Done ()
562
+ }()
563
+ return ctx
564
+ }
0 commit comments