Skip to content

Commit 927d2bc

Browse files
committed
🐛 Metadata objects should always preserve GroupVersionKind
Signed-off-by: Vince Prignano <vincepri@vmware.com>
1 parent b2c90ab commit 927d2bc

File tree

7 files changed

+147
-20
lines changed

7 files changed

+147
-20
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: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,11 +1018,6 @@ func CacheTest(createCacheFunc func(config *rest.Config, opts cache.Options) (ca
10181018
defer deletePod(pod)
10191019
// re-copy the result in so that we can match on it properly
10201020
pod.ObjectMeta.DeepCopyInto(&podMeta.ObjectMeta)
1021-
// NB(directxman12): proto doesn't care typemeta, and
1022-
// partialobjectmetadata is proto, so no typemeta
1023-
// TODO(directxman12): we should paper over this in controller-runtime
1024-
podMeta.APIVersion = ""
1025-
podMeta.Kind = ""
10261021

10271022
By("verifying the object's metadata is received on the channel")
10281023
Eventually(out).Should(Receive(Equal(podMeta)))

pkg/cache/internal/informers_map.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
apierrors "k8s.io/apimachinery/pkg/api/errors"
2727
"k8s.io/apimachinery/pkg/api/meta"
28+
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -34,7 +35,6 @@ import (
3435
"k8s.io/client-go/metadata"
3536
"k8s.io/client-go/rest"
3637
"k8s.io/client-go/tools/cache"
37-
3838
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3939
)
4040

@@ -216,6 +216,13 @@ func (ip *specificInformersMap) addInformerToMap(gvk schema.GroupVersionKind, ob
216216
if err != nil {
217217
return nil, false, err
218218
}
219+
220+
switch obj.(type) {
221+
case *metav1.PartialObjectMetadata, *metav1.PartialObjectMetadataList:
222+
ni = metadataSharedIndexInformerPreserveGVK(gvk, ni)
223+
default:
224+
}
225+
219226
i := &MapEntry{
220227
Informer: ni,
221228
Reader: CacheReader{indexer: ni.GetIndexer(), groupVersionKind: gvk, scopeName: rm.Scope.Name()},
@@ -278,7 +285,12 @@ func createUnstructuredListWatch(gvk schema.GroupVersionKind, ip *specificInform
278285
if err != nil {
279286
return nil, err
280287
}
281-
dynamicClient, err := dynamic.NewForConfig(ip.config)
288+
289+
// If the rest configuration has a negotiated serializer passed in,
290+
// we should remove it and use the one that the dynamic client sets for us.
291+
cfg := rest.CopyConfig(ip.config)
292+
cfg.NegotiatedSerializer = nil
293+
dynamicClient, err := dynamic.NewForConfig(cfg)
282294
if err != nil {
283295
return nil, err
284296
}
@@ -314,8 +326,16 @@ func createMetadataListWatch(gvk schema.GroupVersionKind, ip *specificInformersM
314326
return nil, err
315327
}
316328

329+
// WARNING! Our custom serializer is needed for metadata watches and clients
330+
// otherwise the default one used in client-go removes the GVK on decoding
331+
// which wouldn't allow us to know which object are we actually dealing with.
332+
cfg := rest.CopyConfig(ip.config)
333+
cfg.NegotiatedSerializer = apiutil.SerializerWithDecodedGVK{
334+
WithoutConversionCodecFactory: serializer.WithoutConversionCodecFactory{CodecFactory: metainternalversionscheme.Codecs},
335+
}
336+
317337
// grab the metadata client
318-
client, err := metadata.NewForConfig(ip.config)
338+
client, err := metadata.NewForConfig(cfg)
319339
if err != nil {
320340
return nil, err
321341
}
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: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ type client struct {
159159
}
160160

161161
// 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?
163162
func (c *client) resetGroupVersionKind(obj runtime.Object, gvk schema.GroupVersionKind) {
164163
if gvk != schema.EmptyObjectKind.GroupVersionKind() {
165164
if v, ok := obj.(schema.ObjectKind); ok {
@@ -246,6 +245,8 @@ func (c *client) Get(ctx context.Context, key ObjectKey, obj Object) error {
246245
case *unstructured.Unstructured:
247246
return c.unstructuredClient.Get(ctx, key, obj)
248247
case *metav1.PartialObjectMetadata:
248+
// Metadata only object should always preserve the GVK coming in from the caller.
249+
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
249250
return c.metadataClient.Get(ctx, key, obj)
250251
default:
251252
return c.typedClient.Get(ctx, key, obj)
@@ -258,6 +259,8 @@ func (c *client) List(ctx context.Context, obj ObjectList, opts ...ListOption) e
258259
case *unstructured.UnstructuredList:
259260
return c.unstructuredClient.List(ctx, obj, opts...)
260261
case *metav1.PartialObjectMetadataList:
262+
// Metadata only object should always preserve the GVK coming in from the caller.
263+
defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind())
261264
return c.metadataClient.List(ctx, obj, opts...)
262265
default:
263266
return c.typedClient.List(ctx, obj, opts...)

pkg/client/client_test.go

Lines changed: 14 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

@@ -2389,14 +2393,19 @@ var _ = Describe("Client", func() {
23892393
Expect(err).NotTo(HaveOccurred())
23902394

23912395
By("listing all objects of that type in the cluster")
2392-
metaList := &metav1.PartialObjectMetadataList{}
2393-
metaList.SetGroupVersionKind(schema.GroupVersionKind{
2396+
gvk := schema.GroupVersionKind{
23942397
Group: "apps",
23952398
Version: "v1",
23962399
Kind: "DeploymentList",
2397-
})
2400+
}
2401+
metaList := &metav1.PartialObjectMetadataList{}
2402+
metaList.SetGroupVersionKind(gvk)
23982403
Expect(cl.List(context.Background(), metaList)).NotTo(HaveOccurred())
23992404

2405+
By("validating that the list GVK has been preserved")
2406+
Expect(metaList.GroupVersionKind()).To(Equal(gvk))
2407+
2408+
By("validating that the list has the expected deployment")
24002409
Expect(metaList.Items).NotTo(BeEmpty())
24012410
hasDep := false
24022411
for _, item := range metaList.Items {

0 commit comments

Comments
 (0)