Skip to content

Commit 8f9d0df

Browse files
authored
🐛 Client: Ensure no stale data remains in target object (#1640)
Fixes #1639 The json deserializer of the stdlib and the one from Kube which aims to be compatible won't zero out all field types in the object it deserializes into, for example it lets slices be if the json does not contain that field. This means that if a non-empty variable is used for any api call with the client, the resulting content might be a mixture of previous content and what is on the server. This PR adds a wrapper around the deserializer that will first zero the target object.
1 parent 0a9a777 commit 8f9d0df

File tree

4 files changed

+123
-2
lines changed

4 files changed

+123
-2
lines changed

pkg/client/apiutil/apimachinery.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package apiutil
2121

2222
import (
2323
"fmt"
24+
"reflect"
2425
"sync"
2526

2627
"k8s.io/apimachinery/pkg/api/meta"
@@ -163,9 +164,35 @@ func createRestConfig(gvk schema.GroupVersionKind, isUnstructured bool, baseConf
163164
// Use our own custom serializer.
164165
cfg.NegotiatedSerializer = serializerWithDecodedGVK{serializer.WithoutConversionCodecFactory{CodecFactory: codecs}}
165166
} else {
166-
cfg.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: codecs}
167+
cfg.NegotiatedSerializer = serializerWithTargetZeroingDecode{NegotiatedSerializer: serializer.WithoutConversionCodecFactory{CodecFactory: codecs}}
167168
}
168169
}
169170

170171
return cfg
171172
}
173+
174+
type serializerWithTargetZeroingDecode struct {
175+
runtime.NegotiatedSerializer
176+
}
177+
178+
func (s serializerWithTargetZeroingDecode) DecoderToVersion(serializer runtime.Decoder, r runtime.GroupVersioner) runtime.Decoder {
179+
return targetZeroingDecoder{upstream: s.NegotiatedSerializer.DecoderToVersion(serializer, r)}
180+
}
181+
182+
type targetZeroingDecoder struct {
183+
upstream runtime.Decoder
184+
}
185+
186+
func (t targetZeroingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
187+
zero(into)
188+
return t.upstream.Decode(data, defaults, into)
189+
}
190+
191+
// zero zeros the value of a pointer.
192+
func zero(x interface{}) {
193+
if x == nil {
194+
return
195+
}
196+
res := reflect.ValueOf(x).Elem()
197+
res.Set(reflect.Zero(res.Type()))
198+
}

pkg/client/client_suite_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import (
2222
. "github.com/onsi/ginkgo"
2323
. "github.com/onsi/gomega"
2424
"k8s.io/client-go/kubernetes"
25+
"k8s.io/client-go/kubernetes/scheme"
2526
"k8s.io/client-go/rest"
27+
"sigs.k8s.io/controller-runtime/examples/crd/pkg"
2628
"sigs.k8s.io/controller-runtime/pkg/envtest"
2729
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
2830

@@ -43,14 +45,16 @@ var clientset *kubernetes.Clientset
4345
var _ = BeforeSuite(func() {
4446
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
4547

46-
testenv = &envtest.Environment{}
48+
testenv = &envtest.Environment{CRDDirectoryPaths: []string{"./testdata"}}
4749

4850
var err error
4951
cfg, err = testenv.Start()
5052
Expect(err).NotTo(HaveOccurred())
5153

5254
clientset, err = kubernetes.NewForConfig(cfg)
5355
Expect(err).NotTo(HaveOccurred())
56+
57+
Expect(pkg.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred())
5458
}, 60)
5559

5660
var _ = AfterSuite(func() {

pkg/client/client_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"k8s.io/apimachinery/pkg/types"
3535
kscheme "k8s.io/client-go/kubernetes/scheme"
3636

37+
"sigs.k8s.io/controller-runtime/examples/crd/pkg"
3738
"sigs.k8s.io/controller-runtime/pkg/client"
3839
)
3940

@@ -1397,6 +1398,33 @@ var _ = Describe("Client", func() {
13971398
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
13981399

13991400
})
1401+
1402+
// Test this with an integrated type and a CRD to make sure it covers both proto
1403+
// and json deserialization.
1404+
for idx, object := range []client.Object{&corev1.ConfigMap{}, &pkg.ChaosPod{}} {
1405+
idx, object := idx, object
1406+
It(fmt.Sprintf("should not retain any data in the obj variable that is not on the server for %T", object), func() {
1407+
cl, err := client.New(cfg, client.Options{})
1408+
Expect(err).NotTo(HaveOccurred())
1409+
Expect(cl).NotTo(BeNil())
1410+
1411+
object.SetName(fmt.Sprintf("retain-test-%d", idx))
1412+
object.SetNamespace(ns)
1413+
1414+
By("First creating the object")
1415+
toCreate := object.DeepCopyObject().(client.Object)
1416+
Expect(cl.Create(ctx, toCreate)).NotTo(HaveOccurred())
1417+
1418+
By("Fetching it into a variable that has finalizers set")
1419+
toGetInto := object.DeepCopyObject().(client.Object)
1420+
toGetInto.SetFinalizers([]string{"some-finalizer"})
1421+
Expect(cl.Get(ctx, client.ObjectKeyFromObject(object), toGetInto)).NotTo(HaveOccurred())
1422+
1423+
By("Ensuring the created and the received object are equal")
1424+
Expect(toCreate).Should(Equal(toGetInto))
1425+
})
1426+
}
1427+
14001428
})
14011429

14021430
Context("with unstructured objects", func() {
@@ -1469,6 +1497,30 @@ var _ = Describe("Client", func() {
14691497
err = cl.Get(context.TODO(), key, u)
14701498
Expect(err).To(HaveOccurred())
14711499
})
1500+
1501+
It("should not retain any data in the obj variable that is not on the server", func() {
1502+
object := &unstructured.Unstructured{}
1503+
cl, err := client.New(cfg, client.Options{})
1504+
Expect(err).NotTo(HaveOccurred())
1505+
Expect(cl).NotTo(BeNil())
1506+
1507+
object.SetName("retain-unstructured")
1508+
object.SetNamespace(ns)
1509+
object.SetAPIVersion("chaosapps.metamagical.io/v1")
1510+
object.SetKind("ChaosPod")
1511+
1512+
By("First creating the object")
1513+
toCreate := object.DeepCopyObject().(client.Object)
1514+
Expect(cl.Create(ctx, toCreate)).NotTo(HaveOccurred())
1515+
1516+
By("Fetching it into a variable that has finalizers set")
1517+
toGetInto := object.DeepCopyObject().(client.Object)
1518+
toGetInto.SetFinalizers([]string{"some-finalizer"})
1519+
Expect(cl.Get(ctx, client.ObjectKeyFromObject(object), toGetInto)).NotTo(HaveOccurred())
1520+
1521+
By("Ensuring the created and the received object are equal")
1522+
Expect(toCreate).Should(Equal(toGetInto))
1523+
})
14721524
})
14731525
Context("with metadata objects", func() {
14741526
It("should fetch an existing object for a go struct", func() {
@@ -1547,6 +1599,27 @@ var _ = Describe("Client", func() {
15471599
PIt("should fail if the GVK cannot be mapped to a Resource", func() {
15481600

15491601
})
1602+
1603+
It("should not retain any data in the obj variable that is not on the server", func() {
1604+
cl, err := client.New(cfg, client.Options{})
1605+
Expect(err).NotTo(HaveOccurred())
1606+
Expect(cl).NotTo(BeNil())
1607+
1608+
By("First creating the object")
1609+
toCreate := &pkg.ChaosPod{ObjectMeta: metav1.ObjectMeta{Name: "retain-metadata", Namespace: ns}}
1610+
Expect(cl.Create(ctx, toCreate)).NotTo(HaveOccurred())
1611+
1612+
By("Fetching it into a variable that has finalizers set")
1613+
toGetInto := &metav1.PartialObjectMetadata{
1614+
TypeMeta: metav1.TypeMeta{APIVersion: "chaosapps.metamagical.io/v1", Kind: "ChaosPod"},
1615+
ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: "retain-metadata"},
1616+
}
1617+
toGetInto.SetFinalizers([]string{"some-finalizer"})
1618+
Expect(cl.Get(ctx, client.ObjectKeyFromObject(toGetInto), toGetInto)).NotTo(HaveOccurred())
1619+
1620+
By("Ensuring the created and the received objects metadata are equal")
1621+
Expect(toCreate.ObjectMeta).Should(Equal(toGetInto.ObjectMeta))
1622+
})
15501623
})
15511624
})
15521625

pkg/client/testdata/examplecrd.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: chaospods.chaosapps.metamagical.io
5+
spec:
6+
group: chaosapps.metamagical.io
7+
names:
8+
kind: ChaosPod
9+
plural: chaospods
10+
scope: Namespaced
11+
versions:
12+
- name: "v1"
13+
storage: true
14+
served: true
15+
schema:
16+
openAPIV3Schema:
17+
type: object

0 commit comments

Comments
 (0)