Skip to content

Commit d56c180

Browse files
Merge pull request #55871 from atlassian/unstructured-converter-no-mutation
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Fix potential unexpected object mutation that can lead to data races **What this PR does / why we need it**: In #51526 I introduced an optimization - do a deep copy instead of to and from JSON roundtrip to convert anything that implements `runtime.Unstructured`. I just discovered that the method that is used there `UnstructuredContent()` in both `Unstructured` and `UnstructuredList` may mutate the original object. https://github.com/kubernetes/kubernetes/blob/200875039812d1559555727da74596dc925cfa77/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go#L87-L92 https://github.com/kubernetes/kubernetes/blob/7c10cbc642b47a8f11a74d5178ebbe76a9588cb6/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured_list.go#L58-L75 This is problematic because previously (before #51526) there was no mutation and because this is unexpected and may lead to data races - it is bad behaviour to mutate original object when you just want a copy of it. This PR fixes the issue. Without the fix the tests I've added are failing because when comparison is done original object is not the same: ``` converter_test.go:154: Object changed, diff: object.Object[items]: a: []interface {}{} b: <nil> converter_test.go:154: Object changed, diff: object.Object[items]: a: []interface {}{map[string]interface {}{"kind":"Pod"}} b: <nil> ``` However the underlying issue is not fixed here - `UnstructuredContent()` is brittle and dangerous. Method name does not imply that it mutates data when you call it. And godoc does not mention that either: https://github.com/kubernetes/kubernetes/blob/509df603b18d356777176953e5d160b6f3d0bba9/staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go#L233-L249 Something needs to be done about it IMO. Also `UnstructuredContent()` implementation in `UnstructuredList` does not implement the behaviour required by godoc in `runtime.Unstructured`. **Release note**: ```release-note NONE ``` /kind bug /sig api-machinery /assign @sttts Kubernetes-commit: 2cbb07a4394ef4cfc1666e114674a047023ed178
2 parents 412bd44 + 42ff8b0 commit d56c180

File tree

2 files changed

+40
-8
lines changed

2 files changed

+40
-8
lines changed

pkg/runtime/converter.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,8 @@ func (c *unstructuredConverter) ToUnstructured(obj interface{}) (map[string]inte
411411
var u map[string]interface{}
412412
var err error
413413
if unstr, ok := obj.(Unstructured); ok {
414-
u = DeepCopyJSON(unstr.UnstructuredContent())
414+
// UnstructuredContent() mutates the object so we need to make a copy first
415+
u = unstr.DeepCopyObject().(Unstructured).UnstructuredContent()
415416
} else {
416417
t := reflect.TypeOf(obj)
417418
value := reflect.ValueOf(obj)

pkg/runtime/converter_test.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func doRoundTrip(t *testing.T, item interface{}) {
135135
return
136136
}
137137
unmarshalledObj := reflect.New(reflect.TypeOf(item).Elem()).Interface()
138-
err = json.Unmarshal(data, &unmarshalledObj)
138+
err = json.Unmarshal(data, unmarshalledObj)
139139
if err != nil {
140140
t.Errorf("Error when unmarshaling to object: %v", err)
141141
return
@@ -169,6 +169,38 @@ func TestRoundTrip(t *testing.T) {
169169
testCases := []struct {
170170
obj interface{}
171171
}{
172+
{
173+
obj: &unstructured.UnstructuredList{
174+
Object: map[string]interface{}{
175+
"kind": "List",
176+
},
177+
// Not testing a list with nil Items because items is a non-optional field and hence
178+
// is always marshaled into an empty array which is not equal to nil when unmarshalled and will fail.
179+
// That is expected.
180+
Items: []unstructured.Unstructured{},
181+
},
182+
},
183+
{
184+
obj: &unstructured.UnstructuredList{
185+
Object: map[string]interface{}{
186+
"kind": "List",
187+
},
188+
Items: []unstructured.Unstructured{
189+
{
190+
Object: map[string]interface{}{
191+
"kind": "Pod",
192+
},
193+
},
194+
},
195+
},
196+
},
197+
{
198+
obj: &unstructured.Unstructured{
199+
Object: map[string]interface{}{
200+
"kind": "Pod",
201+
},
202+
},
203+
},
172204
{
173205
obj: &unstructured.Unstructured{
174206
Object: map[string]interface{}{
@@ -260,7 +292,7 @@ func TestRoundTrip(t *testing.T) {
260292
// produces the same object.
261293
func doUnrecognized(t *testing.T, jsonData string, item interface{}, expectedErr error) {
262294
unmarshalledObj := reflect.New(reflect.TypeOf(item).Elem()).Interface()
263-
err := json.Unmarshal([]byte(jsonData), &unmarshalledObj)
295+
err := json.Unmarshal([]byte(jsonData), unmarshalledObj)
264296
if (err != nil) != (expectedErr != nil) {
265297
t.Errorf("Unexpected error when unmarshaling to object: %v, expected: %v", err, expectedErr)
266298
return
@@ -465,11 +497,10 @@ func TestUnrecognized(t *testing.T) {
465497
},
466498
}
467499

468-
for i := range testCases {
469-
doUnrecognized(t, testCases[i].data, testCases[i].obj, testCases[i].err)
470-
if t.Failed() {
471-
break
472-
}
500+
for _, tc := range testCases {
501+
t.Run(tc.data, func(t *testing.T) {
502+
doUnrecognized(t, tc.data, tc.obj, tc.err)
503+
})
473504
}
474505
}
475506

0 commit comments

Comments
 (0)