@@ -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,29 @@ 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
+ }
66
+
51
67
// Builder builds a Controller.
52
68
type Builder = TypedBuilder [reconcile.Request ]
53
69
54
70
// TypedBuilder builds a Controller. The request is the request type
55
71
// that is passed to the workqueue and then to the Reconciler.
56
72
// The workqueue de-duplicates identical requests.
57
73
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 )
74
+ typedClusterWatcher [request ]
75
+ mgr manager.Manager
76
+ ctrlOptions controller.TypedOptions [request ]
77
+ name string
78
+ rawSources []source.TypedSource [request ]
79
+ newController func (name string , mgr manager.Manager , options controller.TypedOptions [request ]) (controller.TypedController [request ], error )
68
80
}
69
81
70
82
// ControllerManagedBy returns a new controller builder that will be started by the provided Manager.
@@ -216,8 +228,12 @@ func (blder *TypedBuilder[request]) WatchesMetadata(
216
228
//
217
229
// WatchesRawSource makes it possible to use typed handlers and predicates with `source.Kind` as well as custom source implementations.
218
230
func (blder * TypedBuilder [request ]) WatchesRawSource (src source.TypedSource [request ]) * TypedBuilder [request ] {
219
- blder .rawSources = append (blder .rawSources , src )
231
+ if src , ok := src .(source.TypedClusterAwareSource [request ]); ok {
232
+ blder .clusterAwareRawSources = append (blder .clusterAwareRawSources , src )
233
+ return blder
234
+ }
220
235
236
+ blder .rawSources = append (blder .rawSources , src )
221
237
return blder
222
238
}
223
239
@@ -279,35 +295,33 @@ func (blder *TypedBuilder[request]) Build(r reconcile.TypedReconciler[request])
279
295
return nil , err
280
296
}
281
297
298
+ if blder .ctrlOptions .EngageWithDefaultCluster == nil {
299
+ blder .ctrlOptions .EngageWithDefaultCluster = blder .mgr .GetControllerOptions ().EngageWithDefaultCluster
300
+ }
301
+
302
+ if blder .ctrlOptions .EngageWithProviderClusters == nil {
303
+ blder .ctrlOptions .EngageWithProviderClusters = blder .mgr .GetControllerOptions ().EngageWithProviderClusters
304
+ }
305
+
282
306
// Set the Watch
283
307
if err := blder .doWatch (); err != nil {
284
308
return nil , err
285
309
}
286
310
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 )
311
+ if * blder .ctrlOptions .EngageWithProviderClusters {
312
+ // wrap as cluster.Aware to be engaged with provider clusters on demand
313
+ if err := blder .mgr .Add (controller .NewTypedMultiClusterController (blder .ctrl , & blder .typedClusterWatcher )); err != nil {
314
+ return nil , err
299
315
}
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
316
}
317
+
318
+ return blder .ctrl , nil
305
319
}
306
320
307
- func (blder * TypedBuilder [request ]) doWatch ( ) error {
321
+ func (cc * typedClusterWatcher [request ]) Watch ( ctx context. Context , cl cluster. Cluster ) error {
308
322
// Reconcile type
309
- if blder .forInput .object != nil {
310
- obj , err := blder . project (blder .forInput .object , blder .forInput .objectProjection )
323
+ if cc .forInput .object != nil {
324
+ obj , err := project (cl , cc .forInput .object , cc .forInput .objectProjection )
311
325
if err != nil {
312
326
return err
313
327
}
@@ -318,20 +332,16 @@ func (blder *TypedBuilder[request]) doWatch() error {
318
332
319
333
var hdler handler.TypedEventHandler [client.Object , request ]
320
334
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 {
335
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
336
+ allPredicates = append (allPredicates , cc .forInput .predicates ... )
337
+ src := & ctxBoundedSyncingSource [ request ]{ ctx : ctx , src : source .TypedKind (cl . GetCache (), obj , hdler , allPredicates ... )}
338
+ if err := cc .ctrl .Watch (src ); err != nil {
325
339
return err
326
340
}
327
341
}
328
342
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 )
343
+ for _ , own := range cc .ownsInput {
344
+ obj , err := project (cl , own .object , own .objectProjection )
335
345
if err != nil {
336
346
return err
337
347
}
@@ -342,38 +352,69 @@ func (blder *TypedBuilder[request]) doWatch() error {
342
352
343
353
var hdler handler.TypedEventHandler [client.Object , request ]
344
354
reflect .ValueOf (& hdler ).Elem ().Set (reflect .ValueOf (handler .EnqueueRequestForOwner (
345
- blder . mgr . GetScheme (), blder . mgr .GetRESTMapper (),
346
- blder .forInput .object ,
355
+ cl . GetScheme (), cl .GetRESTMapper (),
356
+ cc .forInput .object ,
347
357
opts ... ,
348
358
)))
349
- allPredicates := append ([]predicate.Predicate (nil ), blder .globalPredicates ... )
359
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
350
360
allPredicates = append (allPredicates , own .predicates ... )
351
- src := source .TypedKind (blder . mgr . GetCache (), obj , hdler , allPredicates ... )
352
- if err := blder .ctrl .Watch (src ); err != nil {
361
+ src := & ctxBoundedSyncingSource [ request ]{ ctx : ctx , src : source .TypedKind (cl . GetCache (), obj , hdler , allPredicates ... )}
362
+ if err := cc .ctrl .Watch (src ); err != nil {
353
363
return err
354
364
}
355
365
}
356
366
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 )
367
+ for _ , w := range cc .watchesInput {
368
+ projected , err := project (cl , w .obj , w .objectProjection )
363
369
if err != nil {
364
370
return fmt .Errorf ("failed to project for %T: %w" , w .obj , err )
365
371
}
366
- allPredicates := append ([]predicate.Predicate (nil ), blder .globalPredicates ... )
372
+ allPredicates := append ([]predicate.Predicate (nil ), cc .globalPredicates ... )
367
373
allPredicates = append (allPredicates , w .predicates ... )
368
- if err := blder .ctrl .Watch (source .TypedKind (blder .mgr .GetCache (), projected , w .handler , allPredicates ... )); err != nil {
374
+
375
+ h := w .handler
376
+ if deepCopyableHandler , ok := h .(handler.TypedDeepCopyableEventHandler [client.Object , request ]); ok {
377
+ h = deepCopyableHandler .DeepCopyFor (cl )
378
+ }
379
+
380
+ src := & ctxBoundedSyncingSource [request ]{ctx : ctx , src : source .TypedKind (cl .GetCache (), projected , h , allPredicates ... )}
381
+ if err := cc .ctrl .Watch (src ); err != nil {
369
382
return err
370
383
}
371
384
}
372
- for _ , src := range blder .rawSources {
373
- if err := blder .ctrl .Watch (src ); err != nil {
385
+
386
+ for _ , src := range cc .clusterAwareRawSources {
387
+ if err := cc .ctrl .Watch (src ); err != nil {
374
388
return err
375
389
}
376
390
}
391
+
392
+ return nil
393
+ }
394
+
395
+ func (blder * TypedBuilder [request ]) doWatch () error {
396
+ // Pre-checks for a valid configuration
397
+ if len (blder .ownsInput ) > 0 && blder .forInput .object == nil {
398
+ return errors .New ("Owns() can only be used together with For()" )
399
+ }
400
+ if len (blder .watchesInput ) == 0 && blder .forInput .object == nil && len (blder .rawSources ) == 0 {
401
+ return errors .New ("there are no watches configured, controller will never get triggered. Use For(), Owns(), Watches() or WatchesRawSource() to set them up" )
402
+ }
403
+ if ! * blder .ctrlOptions .EngageWithDefaultCluster && len (blder .rawSources ) > 0 {
404
+ return errors .New ("when using a cluster adapter without watching the default cluster, non-cluster-aware custom raw watches are not allowed" )
405
+ }
406
+
407
+ if * blder .ctrlOptions .EngageWithDefaultCluster {
408
+ if err := blder .Watch (unboundedContext , blder .mgr ); err != nil {
409
+ return err
410
+ }
411
+
412
+ for _ , src := range blder .rawSources {
413
+ if err := blder .ctrl .Watch (src ); err != nil {
414
+ return err
415
+ }
416
+ }
417
+ }
377
418
return nil
378
419
}
379
420
@@ -464,3 +505,53 @@ func (blder *TypedBuilder[request]) doController(r reconcile.TypedReconciler[req
464
505
blder .ctrl , err = blder .newController (controllerName , blder .mgr , ctrlOptions )
465
506
return err
466
507
}
508
+
509
+ func project (cl cluster.Cluster , obj client.Object , proj objectProjection ) (client.Object , error ) {
510
+ switch proj {
511
+ case projectAsNormal :
512
+ return obj , nil
513
+ case projectAsMetadata :
514
+ metaObj := & metav1.PartialObjectMetadata {}
515
+ gvk , err := getGvk (obj , cl .GetScheme ())
516
+ if err != nil {
517
+ return nil , fmt .Errorf ("unable to determine GVK of %T for a metadata-only watch: %w" , obj , err )
518
+ }
519
+ metaObj .SetGroupVersionKind (gvk )
520
+ return metaObj , nil
521
+ default :
522
+ panic (fmt .Sprintf ("unexpected projection type %v on type %T, should not be possible since this is an internal field" , proj , obj ))
523
+ }
524
+ }
525
+
526
+ // ctxBoundedSyncingSource implements source.SyncingSource and wraps the ctx
527
+ // passed to the methods into the life-cycle of another context, i.e. stop
528
+ // whenever one of the contexts is done.
529
+ type ctxBoundedSyncingSource [request comparable ] struct {
530
+ ctx context.Context
531
+ src source.TypedSyncingSource [request ]
532
+ }
533
+
534
+ var unboundedContext context.Context = nil //nolint:revive // keep nil explicit for clarity.
535
+
536
+ var _ source.SyncingSource = & ctxBoundedSyncingSource [reconcile.Request ]{}
537
+
538
+ func (s * ctxBoundedSyncingSource [request ]) Start (ctx context.Context , q workqueue.TypedRateLimitingInterface [request ]) error {
539
+ return s .src .Start (joinContexts (ctx , s .ctx ), q )
540
+ }
541
+
542
+ func (s * ctxBoundedSyncingSource [request ]) WaitForSync (ctx context.Context ) error {
543
+ return s .src .WaitForSync (joinContexts (ctx , s .ctx ))
544
+ }
545
+
546
+ func joinContexts (ctx , bound context.Context ) context.Context {
547
+ if bound == unboundedContext {
548
+ return ctx
549
+ }
550
+
551
+ ctx , cancel := context .WithCancel (ctx )
552
+ go func () {
553
+ defer cancel ()
554
+ <- bound .Done ()
555
+ }()
556
+ return ctx
557
+ }
0 commit comments