Skip to content

Commit 2a505b1

Browse files
authored
Merge pull request #2135 from vincepri/add-more-niceties
⚠ Improve builder's capabilities and general UX
2 parents 27270bf + dc62ed5 commit 2a505b1

File tree

3 files changed

+97
-15
lines changed

3 files changed

+97
-15
lines changed

pkg/builder/controller.go

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,20 @@ func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
9797

9898
// OwnsInput represents the information set by Owns method.
9999
type OwnsInput struct {
100+
matchEveryOwner bool
100101
object client.Object
101102
predicates []predicate.Predicate
102103
objectProjection objectProjection
103104
}
104105

105106
// Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to
106-
// create / delete / update events by *reconciling the owner object*. This is the equivalent of calling
107-
// Watches(&source.Kind{Type: <ForType-forInput>}, &handler.EnqueueRequestForOwner{OwnerType: apiType, IsController: true}).
107+
// create / delete / update events by *reconciling the owner object*.
108+
//
109+
// The default behavior reconciles only the first controller-type OwnerReference of the given type.
110+
// Use Owns(object, builder.MatchEveryOwner) to reconcile all owners.
111+
//
112+
// By default, this is the equivalent of calling
113+
// Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner())).
108114
func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
109115
input := OwnsInput{object: object}
110116
for _, opt := range opts {
@@ -123,10 +129,54 @@ type WatchesInput struct {
123129
objectProjection objectProjection
124130
}
125131

126-
// Watches exposes the lower-level ControllerManagedBy Watches functions through the builder. Consider using
127-
// Owns or For instead of Watches directly.
132+
// Watches defines the type of Object to watch, and configures the ControllerManagedBy to respond to create / delete /
133+
// update events by *reconciling the object* with the given EventHandler.
134+
//
135+
// This is the equivalent of calling
136+
// WatchesRawSource(source.Kind(scheme, object), eventhandler, opts...).
137+
func (blder *Builder) Watches(object client.Object, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
138+
src := source.Kind(blder.mgr.GetCache(), object)
139+
return blder.WatchesRawSource(src, eventhandler, opts...)
140+
}
141+
142+
// WatchesMetadata is the same as Watches, but forces the internal cache to only watch PartialObjectMetadata.
143+
//
144+
// This is useful when watching lots of objects, really big objects, or objects for which you only know
145+
// the GVK, but not the structure. You'll need to pass metav1.PartialObjectMetadata to the client
146+
// when fetching objects in your reconciler, otherwise you'll end up with a duplicate structured or unstructured cache.
147+
//
148+
// When watching a resource with metadata only, for example the v1.Pod, you should not Get and List using the v1.Pod type.
149+
// Instead, you should use the special metav1.PartialObjectMetadata type.
150+
//
151+
// ❌ Incorrect:
152+
//
153+
// pod := &v1.Pod{}
154+
// mgr.GetClient().Get(ctx, nsAndName, pod)
155+
//
156+
// ✅ Correct:
157+
//
158+
// pod := &metav1.PartialObjectMetadata{}
159+
// pod.SetGroupVersionKind(schema.GroupVersionKind{
160+
// Group: "",
161+
// Version: "v1",
162+
// Kind: "Pod",
163+
// })
164+
// mgr.GetClient().Get(ctx, nsAndName, pod)
165+
//
166+
// In the first case, controller-runtime will create another cache for the
167+
// concrete type on top of the metadata cache; this increases memory
168+
// consumption and leads to race conditions as caches are not in sync.
169+
func (blder *Builder) WatchesMetadata(object client.Object, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
170+
opts = append(opts, OnlyMetadata)
171+
return blder.Watches(object, eventhandler, opts...)
172+
}
173+
174+
// WatchesRawSource exposes the lower-level ControllerManagedBy Watches functions through the builder.
128175
// Specified predicates are registered only for given source.
129-
func (blder *Builder) Watches(src source.Source, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
176+
//
177+
// STOP! Consider using For(...), Owns(...), Watches(...), WatchesMetadata(...) instead.
178+
// This method is only exposed for more advanced use cases, most users should use higher level functions.
179+
func (blder *Builder) WatchesRawSource(src source.Source, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder {
130180
input := WatchesInput{src: src, eventhandler: eventhandler}
131181
for _, opt := range opts {
132182
opt.ApplyToWatches(&input)
@@ -240,10 +290,14 @@ func (blder *Builder) doWatch() error {
240290
return err
241291
}
242292
src := source.Kind(blder.mgr.GetCache(), obj)
293+
opts := []handler.OwnerOption{}
294+
if !own.matchEveryOwner {
295+
opts = append(opts, handler.OnlyControllerOwner())
296+
}
243297
hdler := handler.EnqueueRequestForOwner(
244298
blder.mgr.GetScheme(), blder.mgr.GetRESTMapper(),
245299
blder.forInput.object,
246-
handler.OnlyControllerOwner(),
300+
opts...,
247301
)
248302
allPredicates := append([]predicate.Predicate(nil), blder.globalPredicates...)
249303
allPredicates = append(allPredicates, own.predicates...)

pkg/builder/controller_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"k8s.io/apimachinery/pkg/types"
3434
"k8s.io/client-go/rest"
3535
"k8s.io/client-go/util/workqueue"
36+
"k8s.io/utils/pointer"
3637

3738
"sigs.k8s.io/controller-runtime/pkg/cache"
3839
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -44,7 +45,6 @@ import (
4445
"sigs.k8s.io/controller-runtime/pkg/predicate"
4546
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4647
"sigs.k8s.io/controller-runtime/pkg/scheme"
47-
"sigs.k8s.io/controller-runtime/pkg/source"
4848
)
4949

5050
type typedNoop struct{}
@@ -118,7 +118,7 @@ var _ = Describe("application", func() {
118118
Expect(err).NotTo(HaveOccurred())
119119

120120
instance, err := ControllerManagedBy(m).
121-
Watches(source.Kind(m.GetCache(), &appsv1.ReplicaSet{}), &handler.EnqueueRequestForObject{}).
121+
Watches(&appsv1.ReplicaSet{}, &handler.EnqueueRequestForObject{}).
122122
Build(noop)
123123
Expect(err).To(MatchError(ContainSubstring("one of For() or Named() must be called")))
124124
Expect(instance).To(BeNil())
@@ -157,7 +157,7 @@ var _ = Describe("application", func() {
157157

158158
instance, err := ControllerManagedBy(m).
159159
Named("my_controller").
160-
Watches(source.Kind(m.GetCache(), &appsv1.ReplicaSet{}), &handler.EnqueueRequestForObject{}).
160+
Watches(&appsv1.ReplicaSet{}, &handler.EnqueueRequestForObject{}).
161161
Build(noop)
162162
Expect(err).NotTo(HaveOccurred())
163163
Expect(instance).NotTo(BeNil())
@@ -362,14 +362,27 @@ var _ = Describe("application", func() {
362362
doReconcileTest(ctx, "3", m, false, bldr)
363363
})
364364

365+
It("should Reconcile Owns objects for every owner", func() {
366+
m, err := manager.New(cfg, manager.Options{})
367+
Expect(err).NotTo(HaveOccurred())
368+
369+
bldr := ControllerManagedBy(m).
370+
For(&appsv1.Deployment{}).
371+
Owns(&appsv1.ReplicaSet{}, MatchEveryOwner)
372+
373+
ctx, cancel := context.WithCancel(context.Background())
374+
defer cancel()
375+
doReconcileTest(ctx, "12", m, false, bldr)
376+
})
377+
365378
It("should Reconcile Watches objects", func() {
366379
m, err := manager.New(cfg, manager.Options{})
367380
Expect(err).NotTo(HaveOccurred())
368381

369382
bldr := ControllerManagedBy(m).
370383
For(&appsv1.Deployment{}).
371384
Watches( // Equivalent of Owns
372-
source.Kind(m.GetCache(), &appsv1.ReplicaSet{}),
385+
&appsv1.ReplicaSet{},
373386
handler.EnqueueRequestForOwner(m.GetScheme(), m.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner()),
374387
)
375388

@@ -385,9 +398,9 @@ var _ = Describe("application", func() {
385398
bldr := ControllerManagedBy(m).
386399
Named("Deployment").
387400
Watches( // Equivalent of For
388-
source.Kind(m.GetCache(), &appsv1.Deployment{}), &handler.EnqueueRequestForObject{}).
401+
&appsv1.Deployment{}, &handler.EnqueueRequestForObject{}).
389402
Watches( // Equivalent of Owns
390-
source.Kind(m.GetCache(), &appsv1.ReplicaSet{}),
403+
&appsv1.ReplicaSet{},
391404
handler.EnqueueRequestForOwner(m.GetScheme(), m.GetRESTMapper(), &appsv1.Deployment{}, handler.OnlyControllerOwner()),
392405
)
393406

@@ -483,7 +496,7 @@ var _ = Describe("application", func() {
483496
bldr := ControllerManagedBy(mgr).
484497
For(&appsv1.Deployment{}, OnlyMetadata).
485498
Owns(&appsv1.ReplicaSet{}, OnlyMetadata).
486-
Watches(source.Kind(mgr.GetCache(), &appsv1.StatefulSet{}),
499+
Watches(&appsv1.StatefulSet{},
487500
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
488501
defer GinkgoRecover()
489502

@@ -645,7 +658,6 @@ func doReconcileTest(ctx context.Context, nameSuffix string, mgr manager.Manager
645658

646659
By("Creating a ReplicaSet")
647660
// Expect a Reconcile when an Owned object is managedObjects.
648-
t := true
649661
rs := &appsv1.ReplicaSet{
650662
ObjectMeta: metav1.ObjectMeta{
651663
Namespace: "default",
@@ -656,7 +668,7 @@ func doReconcileTest(ctx context.Context, nameSuffix string, mgr manager.Manager
656668
Name: deployName,
657669
Kind: "Deployment",
658670
APIVersion: "apps/v1",
659-
Controller: &t,
671+
Controller: pointer.Bool(true),
660672
UID: dep.UID,
661673
},
662674
},

pkg/builder/options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,19 @@ var (
138138
)
139139

140140
// }}}
141+
142+
// MatchEveryOwner determines whether the watch should be filtered based on
143+
// controller ownership. As in, when the OwnerReference.Controller field is set.
144+
//
145+
// If passed as an option,
146+
// the handler receives notification for every owner of the object with the given type.
147+
// If unset (default), the handler receives notification only for the first
148+
// OwnerReference with `Controller: true`.
149+
var MatchEveryOwner = &matchEveryOwner{}
150+
151+
type matchEveryOwner struct{}
152+
153+
// ApplyToOwns applies this configuration to the given OwnsInput options.
154+
func (o matchEveryOwner) ApplyToOwns(opts *OwnsInput) {
155+
opts.matchEveryOwner = true
156+
}

0 commit comments

Comments
 (0)