Skip to content

Commit 2d19f01

Browse files
authored
Merge pull request #1484 from vincepri/wrapmetaonly
🐛 Metadata only objects should always preserve GroupVersionKind
2 parents 4477c71 + dac4475 commit 2d19f01

File tree

8 files changed

+190
-25
lines changed

8 files changed

+190
-25
lines changed

pkg/builder/controller_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,17 @@ var _ = Describe("application", func() {
408408
Owns(&appsv1.ReplicaSet{}, OnlyMetadata).
409409
Watches(&source.Kind{Type: &appsv1.StatefulSet{}},
410410
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
411+
defer GinkgoRecover()
412+
411413
ometa := o.(*metav1.PartialObjectMetadata)
412414
statefulSetMaps <- ometa
415+
416+
// Validate that the GVK is not empty when dealing with PartialObjectMetadata objects.
417+
Expect(o.GetObjectKind().GroupVersionKind()).To(Equal(schema.GroupVersionKind{
418+
Group: "apps",
419+
Version: "v1",
420+
Kind: "StatefulSet",
421+
}))
413422
return nil
414423
}),
415424
OnlyMetadata)

pkg/cache/cache_test.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,11 +1135,6 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
11351135
defer deletePod(pod)
11361136
// re-copy the result in so that we can match on it properly
11371137
pod.ObjectMeta.DeepCopyInto(&podMeta.ObjectMeta)
1138-
// NB(directxman12): proto doesn't care typemeta, and
1139-
// partialobjectmetadata is proto, so no typemeta
1140-
// TODO(directxman12): we should paper over this in controller-runtime
1141-
podMeta.APIVersion = ""
1142-
podMeta.Kind = ""
11431138

11441139
By("verifying the object's metadata is received on the channel")
11451140
Eventually(out).Should(Receive(Equal(podMeta)))
@@ -1173,20 +1168,31 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
11731168

11741169
By("listing Pods with restartPolicyOnFailure")
11751170
listObj := &kmetav1.PartialObjectMetadataList{}
1176-
listObj.SetGroupVersionKind(schema.GroupVersionKind{
1171+
gvk := schema.GroupVersionKind{
11771172
Group: "",
11781173
Version: "v1",
11791174
Kind: "PodList",
1180-
})
1175+
}
1176+
listObj.SetGroupVersionKind(gvk)
11811177
err = informer.List(context.Background(), listObj,
11821178
client.MatchingFields{"metadata.labels.test-label": "test-pod-3"})
11831179
Expect(err).To(Succeed())
11841180

1181+
By("verifying that the GVK has been preserved for the list object")
1182+
Expect(listObj.GroupVersionKind()).To(Equal(gvk))
1183+
11851184
By("verifying that the returned pods have correct restart policy")
11861185
Expect(listObj.Items).NotTo(BeEmpty())
11871186
Expect(listObj.Items).Should(HaveLen(1))
11881187
actual := listObj.Items[0]
11891188
Expect(actual.GetName()).To(Equal("test-pod-3"))
1189+
1190+
By("verifying that the GVK has been preserved for the item in the list")
1191+
Expect(actual.GroupVersionKind()).To(Equal(schema.GroupVersionKind{
1192+
Group: "",
1193+
Version: "v1",
1194+
Kind: "Pod",
1195+
}))
11901196
}, 3)
11911197

11921198
It("should allow for get informer to be cancelled", func() {

pkg/cache/internal/informers_map.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
"k8s.io/client-go/metadata"
3535
"k8s.io/client-go/rest"
3636
"k8s.io/client-go/tools/cache"
37-
3837
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3938
)
4039

@@ -222,6 +221,13 @@ func (ip *specificInformersMap) addInformerToMap(gvk schema.GroupVersionKind, ob
222221
if err != nil {
223222
return nil, false, err
224223
}
224+
225+
switch obj.(type) {
226+
case *metav1.PartialObjectMetadata, *metav1.PartialObjectMetadataList:
227+
ni = metadataSharedIndexInformerPreserveGVK(gvk, ni)
228+
default:
229+
}
230+
225231
i := &MapEntry{
226232
Informer: ni,
227233
Reader: CacheReader{indexer: ni.GetIndexer(), groupVersionKind: gvk, scopeName: rm.Scope.Name()},
@@ -286,7 +292,12 @@ func createUnstructuredListWatch(gvk schema.GroupVersionKind, ip *specificInform
286292
if err != nil {
287293
return nil, err
288294
}
289-
dynamicClient, err := dynamic.NewForConfig(ip.config)
295+
296+
// If the rest configuration has a negotiated serializer passed in,
297+
// we should remove it and use the one that the dynamic client sets for us.
298+
cfg := rest.CopyConfig(ip.config)
299+
cfg.NegotiatedSerializer = nil
300+
dynamicClient, err := dynamic.NewForConfig(cfg)
290301
if err != nil {
291302
return nil, err
292303
}
@@ -324,8 +335,13 @@ func createMetadataListWatch(gvk schema.GroupVersionKind, ip *specificInformersM
324335
return nil, err
325336
}
326337

338+
// Always clear the negotiated serializer and use the one
339+
// set from the metadata client.
340+
cfg := rest.CopyConfig(ip.config)
341+
cfg.NegotiatedSerializer = nil
342+
327343
// grab the metadata client
328-
client, err := metadata.NewForConfig(ip.config)
344+
client, err := metadata.NewForConfig(cfg)
329345
if err != nil {
330346
return nil, err
331347
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package internal
18+
19+
import (
20+
"time"
21+
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"k8s.io/client-go/tools/cache"
24+
)
25+
26+
func metadataSharedIndexInformerPreserveGVK(gvk schema.GroupVersionKind, si cache.SharedIndexInformer) cache.SharedIndexInformer {
27+
return &sharedInformerWrapper{
28+
gvk: gvk,
29+
SharedIndexInformer: si,
30+
}
31+
}
32+
33+
type sharedInformerWrapper struct {
34+
gvk schema.GroupVersionKind
35+
cache.SharedIndexInformer
36+
}
37+
38+
func (s *sharedInformerWrapper) AddEventHandler(handler cache.ResourceEventHandler) {
39+
s.SharedIndexInformer.AddEventHandler(&handlerPreserveGVK{s.gvk, handler})
40+
}
41+
42+
func (s *sharedInformerWrapper) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) {
43+
s.SharedIndexInformer.AddEventHandlerWithResyncPeriod(&handlerPreserveGVK{s.gvk, handler}, resyncPeriod)
44+
}
45+
46+
type handlerPreserveGVK struct {
47+
gvk schema.GroupVersionKind
48+
cache.ResourceEventHandler
49+
}
50+
51+
func (h *handlerPreserveGVK) resetGroupVersionKind(obj interface{}) {
52+
if v, ok := obj.(schema.ObjectKind); ok {
53+
v.SetGroupVersionKind(h.gvk)
54+
}
55+
}
56+
57+
func (h *handlerPreserveGVK) OnAdd(obj interface{}) {
58+
h.resetGroupVersionKind(obj)
59+
h.ResourceEventHandler.OnAdd(obj)
60+
}
61+
62+
func (h *handlerPreserveGVK) OnUpdate(oldObj, newObj interface{}) {
63+
h.resetGroupVersionKind(oldObj)
64+
h.resetGroupVersionKind(newObj)
65+
h.ResourceEventHandler.OnUpdate(oldObj, newObj)
66+
}
67+
68+
func (h *handlerPreserveGVK) OnDelete(obj interface{}) {
69+
h.resetGroupVersionKind(obj)
70+
h.ResourceEventHandler.OnDelete(obj)
71+
}

pkg/client/apiutil/apimachinery.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,24 @@ func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersi
118118
// with the given GroupVersionKind. The REST client will be configured to use the negotiated serializer from
119119
// baseConfig, if set, otherwise a default serializer will be set.
120120
func RESTClientForGVK(gvk schema.GroupVersionKind, isUnstructured bool, baseConfig *rest.Config, codecs serializer.CodecFactory) (rest.Interface, error) {
121-
cfg := createRestConfig(gvk, isUnstructured, baseConfig)
122-
if cfg.NegotiatedSerializer == nil {
123-
cfg.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: codecs}
124-
}
125-
return rest.RESTClientFor(cfg)
121+
return rest.RESTClientFor(createRestConfig(gvk, isUnstructured, baseConfig, codecs))
122+
}
123+
124+
// serializerWithDecodedGVK is a CodecFactory that overrides the DecoderToVersion of a WithoutConversionCodecFactory
125+
// in order to avoid clearing the GVK from the decoded object.
126+
//
127+
// See https://github.com/kubernetes/kubernetes/issues/80609.
128+
type serializerWithDecodedGVK struct {
129+
serializer.WithoutConversionCodecFactory
130+
}
131+
132+
// DecoderToVersion returns an decoder that does not do conversion.
133+
func (f serializerWithDecodedGVK) DecoderToVersion(serializer runtime.Decoder, _ runtime.GroupVersioner) runtime.Decoder {
134+
return serializer
126135
}
127136

128137
//createRestConfig copies the base config and updates needed fields for a new rest config
129-
func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConfig *rest.Config) *rest.Config {
138+
func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConfig *rest.Config, codecs serializer.CodecFactory) *rest.Config {
130139
gv := gvk.GroupVersion()
131140

132141
cfg := rest.CopyConfig(baseConfig)
@@ -147,5 +156,16 @@ func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConf
147156
}
148157
protobufSchemeLock.RUnlock()
149158
}
159+
160+
if cfg.NegotiatedSerializer == nil {
161+
if isUnstructured {
162+
// If the object is unstructured, we need to preserve the GVK information.
163+
// Use our own custom serializer.
164+
cfg.NegotiatedSerializer = serializerWithDecodedGVK{serializer.WithoutConversionCodecFactory{CodecFactory: codecs}}
165+
} else {
166+
cfg.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: codecs}
167+
}
168+
}
169+
150170
return cfg
151171
}

pkg/client/client.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package client
1919
import (
2020
"context"
2121
"fmt"
22+
"strings"
2223

2324
"k8s.io/apimachinery/pkg/api/meta"
2425
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -159,7 +160,6 @@ type client struct {
159160
}
160161

161162
// resetGroupVersionKind is a helper function to restore and preserve GroupVersionKind on an object.
162-
// TODO(vincepri): Remove this function and its calls once controller-runtime dependencies are upgraded to 1.16?
163163
func (c *client) resetGroupVersionKind(obj runtime.Object, gvk schema.GroupVersionKind) {
164164
if gvk != schema.EmptyObjectKind.GroupVersionKind() {
165165
if v, ok := obj.(schema.ObjectKind); ok {
@@ -246,6 +246,8 @@ func (c *client) Get(ctx context.Context, key ObjectKey, obj Object) error {
246246
case *unstructured.Unstructured:
247247
return c.unstructuredClient.Get(ctx, key, obj)
248248
case *metav1.PartialObjectMetadata:
249+
// Metadata only object should always preserve the GVK coming in from the caller.
250+
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
249251
return c.metadataClient.Get(ctx, key, obj)
250252
default:
251253
return c.typedClient.Get(ctx, key, obj)
@@ -254,11 +256,33 @@ func (c *client) Get(ctx context.Context, key ObjectKey, obj Object) error {
254256

255257
// List implements client.Client
256258
func (c *client) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
257-
switch obj.(type) {
259+
switch x := obj.(type) {
258260
case *unstructured.UnstructuredList:
259261
return c.unstructuredClient.List(ctx, obj, opts...)
260262
case *metav1.PartialObjectMetadataList:
261-
return c.metadataClient.List(ctx, obj, opts...)
263+
// Metadata only object should always preserve the GVK.
264+
gvk := obj.GetObjectKind().GroupVersionKind()
265+
defer c.resetGroupVersionKind(obj, gvk)
266+
267+
// Call the list client.
268+
if err := c.metadataClient.List(ctx, obj, opts...); err != nil {
269+
return err
270+
}
271+
272+
// Restore the GVK for each item in the list.
273+
itemGVK := schema.GroupVersionKind{
274+
Group: gvk.Group,
275+
Version: gvk.Version,
276+
// TODO: this is producing unsafe guesses that don't actually work,
277+
// but it matches ~99% of the cases out there.
278+
Kind: strings.TrimSuffix(gvk.Kind, "List"),
279+
}
280+
for i := range x.Items {
281+
item := &x.Items[i]
282+
item.SetGroupVersionKind(itemGVK)
283+
}
284+
285+
return nil
262286
default:
263287
return c.typedClient.List(ctx, obj, opts...)
264288
}

pkg/client/client_cache.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ type resourceMeta struct {
133133
// isNamespaced returns true if the type is namespaced
134134
func (r *resourceMeta) isNamespaced() bool {
135135
return r.mapping.Scope.Name() != meta.RESTScopeNameRoot
136-
137136
}
138137

139138
// resource returns the resource name of the type

pkg/client/client_test.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,16 +1558,20 @@ var _ = Describe("Client", func() {
15581558

15591559
By("fetching the created Deployment")
15601560
var actual metav1.PartialObjectMetadata
1561-
actual.SetGroupVersionKind(schema.GroupVersionKind{
1561+
gvk := schema.GroupVersionKind{
15621562
Group: "apps",
15631563
Version: "v1",
15641564
Kind: "Deployment",
1565-
})
1565+
}
1566+
actual.SetGroupVersionKind(gvk)
15661567
key := client.ObjectKey{Namespace: ns, Name: dep.Name}
15671568
err = cl.Get(context.TODO(), key, &actual)
15681569
Expect(err).NotTo(HaveOccurred())
15691570
Expect(actual).NotTo(BeNil())
15701571

1572+
By("validating that the GVK has been preserved")
1573+
Expect(actual.GroupVersionKind()).To(Equal(gvk))
1574+
15711575
By("validating the fetched deployment equals the created one")
15721576
Expect(metaOnlyFromObj(dep, scheme)).To(Equal(&actual))
15731577

@@ -1676,6 +1680,11 @@ var _ = Describe("Client", func() {
16761680
Expect(deps.Items).NotTo(BeEmpty())
16771681
hasDep := false
16781682
for _, item := range deps.Items {
1683+
Expect(item.GroupVersionKind()).To(Equal(schema.GroupVersionKind{
1684+
Group: "apps",
1685+
Kind: "Deployment",
1686+
Version: "v1",
1687+
}))
16791688
if item.GetName() == dep.Name && item.GetNamespace() == dep.Namespace {
16801689
hasDep = true
16811690
break
@@ -2389,17 +2398,28 @@ var _ = Describe("Client", func() {
23892398
Expect(err).NotTo(HaveOccurred())
23902399

23912400
By("listing all objects of that type in the cluster")
2392-
metaList := &metav1.PartialObjectMetadataList{}
2393-
metaList.SetGroupVersionKind(schema.GroupVersionKind{
2401+
gvk := schema.GroupVersionKind{
23942402
Group: "apps",
23952403
Version: "v1",
23962404
Kind: "DeploymentList",
2397-
})
2405+
}
2406+
metaList := &metav1.PartialObjectMetadataList{}
2407+
metaList.SetGroupVersionKind(gvk)
23982408
Expect(cl.List(context.Background(), metaList)).NotTo(HaveOccurred())
23992409

2410+
By("validating that the list GVK has been preserved")
2411+
Expect(metaList.GroupVersionKind()).To(Equal(gvk))
2412+
2413+
By("validating that the list has the expected deployment")
24002414
Expect(metaList.Items).NotTo(BeEmpty())
24012415
hasDep := false
24022416
for _, item := range metaList.Items {
2417+
Expect(item.GroupVersionKind()).To(Equal(schema.GroupVersionKind{
2418+
Group: "apps",
2419+
Version: "v1",
2420+
Kind: "Deployment",
2421+
}))
2422+
24032423
if item.Name == dep.Name && item.Namespace == dep.Namespace {
24042424
hasDep = true
24052425
break

0 commit comments

Comments
 (0)