Skip to content

Commit c74742d

Browse files
author
Alexander Zielenski
committed
expose builder api for stripping managedfields, annotations
1 parent 7ba3e55 commit c74742d

File tree

5 files changed

+430
-9
lines changed

5 files changed

+430
-9
lines changed

pkg/builder/controller.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type ForInput struct {
7171
object client.Object
7272
predicates []predicate.Predicate
7373
objectProjection objectProjection
74+
metadataFilter manager.MetadataFilter
7475
err error
7576
}
7677

@@ -97,6 +98,7 @@ type OwnsInput struct {
9798
object client.Object
9899
predicates []predicate.Predicate
99100
objectProjection objectProjection
101+
metadataFilter manager.MetadataFilter
100102
}
101103

102104
// Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to
@@ -118,6 +120,7 @@ type WatchesInput struct {
118120
eventhandler handler.EventHandler
119121
predicates []predicate.Predicate
120122
objectProjection objectProjection
123+
metadataFilter manager.MetadataFilter
121124
}
122125

123126
// Watches exposes the lower-level ControllerManagedBy Watches functions through the builder. Consider using
@@ -216,6 +219,23 @@ func (blder *Builder) project(obj client.Object, proj objectProjection) (client.
216219
}
217220
}
218221

222+
// Finds the SharedIndexInformer used for the given source, and installs
223+
// an object preprocessor to strip specified fields before objects are stored
224+
// in the Store, or broadcasted to listeners.
225+
func (blder *Builder) installMetadataFilterForKindSource(kind *source.Kind, filter manager.MetadataFilter) error {
226+
gvk, err := apiutil.GVKForObject(kind.Type, blder.mgr.GetScheme())
227+
if err != nil {
228+
return fmt.Errorf("failed to find gvk for object: %v", kind.Type)
229+
}
230+
231+
// Set the configuration onto the manager.
232+
// Manager is then responsible for applying it to the cache before starting
233+
// any informers.
234+
blder.mgr.AddMetadataFilterForKind(gvk, filter)
235+
236+
return nil
237+
}
238+
219239
func (blder *Builder) doWatch() error {
220240
// Reconcile type
221241
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
@@ -229,6 +249,12 @@ func (blder *Builder) doWatch() error {
229249
return err
230250
}
231251

252+
// Must be called after Watch which populates the source's cache
253+
if err := blder.installMetadataFilterForKindSource(
254+
src, blder.forInput.metadataFilter); err != nil {
255+
return err
256+
}
257+
232258
// Watches the managed types
233259
for _, own := range blder.ownsInput {
234260
typeForSrc, err := blder.project(own.object, own.objectProjection)
@@ -245,6 +271,12 @@ func (blder *Builder) doWatch() error {
245271
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
246272
return err
247273
}
274+
275+
// Must be called after Watch which populates the source's cache
276+
if err := blder.installMetadataFilterForKindSource(
277+
src, own.metadataFilter); err != nil {
278+
return err
279+
}
248280
}
249281

250282
// Do the watch requests
@@ -264,6 +296,16 @@ func (blder *Builder) doWatch() error {
264296
if err := blder.ctrl.Watch(w.src, w.eventhandler, allPredicates...); err != nil {
265297
return err
266298
}
299+
300+
if srckind, ok := w.src.(*source.Kind); ok {
301+
// We can only apply projections to source.Kind types
302+
// Must be called after Watch which populates the source's cache
303+
if err := blder.installMetadataFilterForKindSource(
304+
srckind, w.metadataFilter); err != nil {
305+
return err
306+
}
307+
}
308+
267309
}
268310
return nil
269311
}

pkg/builder/controller_test.go

Lines changed: 182 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,139 @@ func (l *testLogger) WithName(name string) logr.LogSink {
7575
return l
7676
}
7777

78+
func doStripMetadataTestWithOptions(mgr manager.Manager, suffix string, opts ...*metadataFilterInjectorFunc) {
79+
statefulSetMaps := make(chan *metav1.PartialObjectMetadata)
80+
forOpts := []ForOption{OnlyMetadata}
81+
ownOpts := []OwnsOption{OnlyMetadata}
82+
watchOpts := []WatchesOption{OnlyMetadata}
83+
84+
expectManagedFields := true
85+
expectAnnotations := true
86+
87+
for _, v := range opts {
88+
forOpts = append(forOpts, *v)
89+
ownOpts = append(ownOpts, *v)
90+
watchOpts = append(watchOpts, *v)
91+
92+
expectManagedFields = expectManagedFields && v != &WithoutManagedFields
93+
expectAnnotations = expectAnnotations && v != &WithoutAnnotations
94+
}
95+
96+
testObject := func(o metav1.Object) bool {
97+
// Validate that ManagedFields is nil
98+
if expectAnnotations {
99+
Expect(o.GetAnnotations()).ToNot(BeNil())
100+
} else {
101+
Expect((o.GetAnnotations())).To(BeNil())
102+
}
103+
if expectManagedFields {
104+
Expect((o.GetManagedFields())).ToNot(BeNil())
105+
} else {
106+
Expect((o.GetManagedFields())).To(BeNil())
107+
}
108+
return true
109+
}
110+
111+
set := &appsv1.StatefulSet{
112+
ObjectMeta: metav1.ObjectMeta{
113+
Namespace: "default",
114+
Name: "test1-" + suffix,
115+
Labels: map[string]string{
116+
"foo": "bar",
117+
},
118+
Annotations: map[string]string{
119+
"foo": "bar",
120+
},
121+
},
122+
Spec: appsv1.StatefulSetSpec{
123+
Selector: &metav1.LabelSelector{
124+
MatchLabels: map[string]string{"foo": "bar"},
125+
},
126+
Template: corev1.PodTemplateSpec{
127+
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
128+
Spec: corev1.PodSpec{
129+
Containers: []corev1.Container{
130+
{
131+
Name: "nginx",
132+
Image: "nginx",
133+
},
134+
},
135+
},
136+
},
137+
},
138+
}
139+
140+
bldr := ControllerManagedBy(mgr).
141+
For(&appsv1.Deployment{}, forOpts...).
142+
Owns(&appsv1.ReplicaSet{}, ownOpts...).
143+
Watches(&source.Kind{Type: &appsv1.StatefulSet{}},
144+
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
145+
defer GinkgoRecover()
146+
147+
if o.GetName() != set.Name {
148+
// I suppose the objects in the test cluster are not reset
149+
// in-between tests?
150+
return nil
151+
}
152+
153+
ometa := o.(*metav1.PartialObjectMetadata)
154+
Expect(ometa.Name).To(Equal(set.Name))
155+
Expect(ometa.Namespace).To(Equal(set.Namespace))
156+
Expect(ometa.Labels).To(Equal(set.Labels))
157+
statefulSetMaps <- ometa
158+
159+
// Validate that the GVK is not empty when dealing with PartialObjectMetadata objects.
160+
Expect(o.GetObjectKind().GroupVersionKind()).To(Equal(schema.GroupVersionKind{
161+
Group: "apps",
162+
Version: "v1",
163+
Kind: "StatefulSet",
164+
}))
165+
166+
testObject(o)
167+
return nil
168+
}),
169+
watchOpts...)
170+
171+
ctx, cancel := context.WithCancel(context.Background())
172+
defer cancel()
173+
174+
doReconcileTest(ctx, suffix, mgr, true, bldr)
175+
176+
By("Creating a new stateful set")
177+
178+
err := mgr.GetClient().Create(context.TODO(), set)
179+
Expect(err).NotTo(HaveOccurred())
180+
181+
By("Checking that 'Owns' Deployments have correct projection")
182+
deployment, err := bldr.project(&appsv1.Deployment{}, projectAsMetadata)
183+
Expect(err).NotTo(HaveOccurred())
184+
err = mgr.GetCache().Get(ctx, types.NamespacedName{
185+
Namespace: "default",
186+
Name: "deploy-name-" + suffix,
187+
}, deployment)
188+
Expect(err).NotTo(HaveOccurred())
189+
testObject(deployment)
190+
191+
By("Checking that 'For' ReplicaSets have correct projection")
192+
rs, err := bldr.project(&appsv1.ReplicaSet{}, projectAsMetadata)
193+
Expect(err).NotTo(HaveOccurred())
194+
err = mgr.GetCache().Get(ctx, types.NamespacedName{
195+
Namespace: "default",
196+
Name: "rs-name-" + suffix,
197+
}, rs)
198+
Expect(err).NotTo(HaveOccurred())
199+
testObject(rs)
200+
201+
By("Checking that the mapping function has been called")
202+
Eventually(func() bool {
203+
metaSet := <-statefulSetMaps
204+
Expect(metaSet.Name).To(Equal(set.Name))
205+
Expect(metaSet.Namespace).To(Equal(set.Namespace))
206+
Expect(metaSet.Labels).To(Equal(set.Labels))
207+
return true
208+
}).Should(BeTrue())
209+
}
210+
78211
var _ = Describe("application", func() {
79212
BeforeEach(func() {
80213
newController = controller.New
@@ -417,6 +550,38 @@ var _ = Describe("application", func() {
417550
doReconcileTest(ctx, "6", mgr, true, bldr1, bldr2)
418551
})
419552

553+
It("should support keeping metadata fields if not all controllers agree to strip them", func() {
554+
bldr1 := ControllerManagedBy(mgr).For(&appsv1.Deployment{}, OnlyMetadata, WithoutManagedFields)
555+
bldr2 := ControllerManagedBy(mgr).For(&appsv1.Deployment{}, OnlyMetadata)
556+
557+
ctx, cancel := context.WithCancel(context.Background())
558+
defer cancel()
559+
560+
doReconcileTest(ctx, "7-0", mgr, true, bldr1, bldr2)
561+
562+
By("Checking that Deployments metadata still has managed fields")
563+
deployment, err := bldr1.project(&appsv1.Deployment{}, projectAsMetadata)
564+
Expect(err).NotTo(HaveOccurred())
565+
err = mgr.GetCache().Get(ctx, types.NamespacedName{
566+
Namespace: "default",
567+
Name: "deploy-name-" + "7-0",
568+
}, deployment)
569+
Expect(err).NotTo(HaveOccurred())
570+
Expect(deployment.GetManagedFields()).ToNot(BeNil())
571+
})
572+
573+
It("should support stripping annoatations for metadata projections", func() {
574+
doStripMetadataTestWithOptions(mgr, "7-1", &WithoutAnnotations)
575+
})
576+
577+
It("should support stripping managed fields options for metadata projections", func() {
578+
doStripMetadataTestWithOptions(mgr, "7-2", &WithoutManagedFields)
579+
})
580+
581+
It("should support all stripping options for metadata projections", func() {
582+
doStripMetadataTestWithOptions(mgr, "7-3", &WithoutAnnotations, &WithoutManagedFields)
583+
})
584+
420585
It("should support watching For, Owns, and Watch as metadata", func() {
421586
statefulSetMaps := make(chan *metav1.PartialObjectMetadata)
422587

@@ -427,15 +592,17 @@ var _ = Describe("application", func() {
427592
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
428593
defer GinkgoRecover()
429594

430-
ometa := o.(*metav1.PartialObjectMetadata)
431-
statefulSetMaps <- ometa
432-
433-
// Validate that the GVK is not empty when dealing with PartialObjectMetadata objects.
434-
Expect(o.GetObjectKind().GroupVersionKind()).To(Equal(schema.GroupVersionKind{
435-
Group: "apps",
436-
Version: "v1",
437-
Kind: "StatefulSet",
438-
}))
595+
if o.GetName() == "test1" {
596+
ometa := o.(*metav1.PartialObjectMetadata)
597+
statefulSetMaps <- ometa
598+
599+
// Validate that the GVK is not empty when dealing with PartialObjectMetadata objects.
600+
Expect(o.GetObjectKind().GroupVersionKind()).To(Equal(schema.GroupVersionKind{
601+
Group: "apps",
602+
Version: "v1",
603+
Kind: "StatefulSet",
604+
}))
605+
}
439606
return nil
440607
}),
441608
OnlyMetadata)
@@ -559,6 +726,9 @@ func doReconcileTest(ctx context.Context, nameSuffix string, mgr manager.Manager
559726
ObjectMeta: metav1.ObjectMeta{
560727
Namespace: "default",
561728
Name: deployName,
729+
Annotations: map[string]string{
730+
"foo": "bar",
731+
},
562732
},
563733
Spec: appsv1.DeploymentSpec{
564734
Selector: &metav1.LabelSelector{
@@ -601,6 +771,9 @@ func doReconcileTest(ctx context.Context, nameSuffix string, mgr manager.Manager
601771
UID: dep.UID,
602772
},
603773
},
774+
Annotations: map[string]string{
775+
"foo": "bar",
776+
},
604777
},
605778
Spec: appsv1.ReplicaSetSpec{
606779
Selector: dep.Spec.Selector,

pkg/builder/options.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package builder
1818

1919
import (
20+
"sigs.k8s.io/controller-runtime/pkg/manager"
2021
"sigs.k8s.io/controller-runtime/pkg/predicate"
2122
)
2223

@@ -115,3 +116,50 @@ var (
115116
)
116117

117118
// }}}
119+
120+
// {{{ Metadata Filter Options
121+
122+
type metadataFilterInjectorFunc func(*manager.MetadataFilter)
123+
124+
// ApplyToFor applies this configuration to the given ForInput options.
125+
func (p metadataFilterInjectorFunc) ApplyToFor(opts *ForInput) {
126+
p(&opts.metadataFilter)
127+
}
128+
129+
// ApplyToOwns applies this configuration to the given OwnsInput options.
130+
func (p metadataFilterInjectorFunc) ApplyToOwns(opts *OwnsInput) {
131+
p(&opts.metadataFilter)
132+
}
133+
134+
// ApplyToWatches applies this configuration to the given WatchesInput options.
135+
func (p metadataFilterInjectorFunc) ApplyToWatches(opts *WatchesInput) {
136+
p(&opts.metadataFilter)
137+
}
138+
139+
var (
140+
// WithoutManagedFields informs the cache that managedFields are not required
141+
// to be stored, so they will be removed before being stored in the cache.
142+
// If multiple controllers watch the same GVK with the same source type,
143+
// then managedFields are stripped if the last constructed controller
144+
// requested it.
145+
WithoutManagedFields = metadataFilterInjectorFunc(func(filter *manager.MetadataFilter) { filter.RemoveManagedFields = true })
146+
147+
// WithoutAnnotations informs the cache that annotations metadata are not
148+
// required to be stored, so they will be removed before being stored in
149+
// the cache.
150+
//
151+
// If multiple controllers watch the same GVK with the same source type,
152+
// then annotations are stripped if the last constructed controller
153+
// requested it.
154+
WithoutAnnotations = metadataFilterInjectorFunc(func(filter *manager.MetadataFilter) { filter.RemoveAnnotations = true })
155+
156+
_ ForOption = WithoutManagedFields
157+
_ OwnsOption = WithoutManagedFields
158+
_ WatchesOption = WithoutManagedFields
159+
160+
_ ForOption = WithoutAnnotations
161+
_ OwnsOption = WithoutAnnotations
162+
_ WatchesOption = WithoutAnnotations
163+
)
164+
165+
// }}}

0 commit comments

Comments
 (0)