From e3d9b6cadfd508c2805bb417d11e24919bebc817 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 31 Jan 2025 01:19:09 +0300 Subject: [PATCH] K8s/Unstructured: Avoid panic in DeepCopy (#99840) --- .../apis/common/v0alpha1/unstructured.go | 54 ++++++++++++++++++- .../apis/common/v0alpha1/unstructured_test.go | 44 +++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go index e779c5fafd3..17417fd85fa 100644 --- a/pkg/apimachinery/apis/common/v0alpha1/unstructured.go +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured.go @@ -2,6 +2,7 @@ package v0alpha1 import ( "encoding/json" + "reflect" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" runtime "k8s.io/apimachinery/pkg/runtime" @@ -63,7 +64,7 @@ func (u *Unstructured) DeepCopy() *Unstructured { } out := new(Unstructured) *out = *u - out.Object = runtime.DeepCopyJSON(u.Object) + out.Object = deepCopyJSONValue(u.Object).(map[string]interface{}) return out } @@ -72,6 +73,57 @@ func (u *Unstructured) DeepCopyInto(out *Unstructured) { *out = *clone } +// Copied from: +// +// runtime.DeepCopyJSON(u.Object) +// +// BUT this avoids panic on int +func deepCopyJSONValue(x interface{}) interface{} { + switch x := x.(type) { + case map[string]interface{}: + if x == nil { + // Typed nil - an interface{} that contains a type map[string]interface{} with a value of nil + return x + } + clone := make(map[string]interface{}, len(x)) + for k, v := range x { + clone[k] = deepCopyJSONValue(v) + } + return clone + case []interface{}: + if x == nil { + // Typed nil - an interface{} that contains a type []interface{} with a value of nil + return x + } + clone := make([]interface{}, len(x)) + for i, v := range x { + clone[i] = deepCopyJSONValue(v) + } + return clone + case string, int64, bool, float64, nil, json.Number: + return x + + // Keep more numbers + case int, int8, int16, int32, float32, uint, uint16, uint32, uint64, uint8: + return x + + case runtime.Object: + return x.DeepCopyObject() + + default: + // fallback to reflection + val := reflect.ValueOf(x).Elem() + cpy := reflect.New(val.Type()) + cpy.Elem().Set(val) + + // Using the , for the type conversion ensures that it doesn't panic if it can't be converted + if obj, ok := cpy.Interface().(runtime.Object); ok { + return obj + } + return x + } +} + func (u *Unstructured) Set(field string, value interface{}) { if u.Object == nil { u.Object = make(map[string]interface{}) diff --git a/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go new file mode 100644 index 00000000000..1698bd081c0 --- /dev/null +++ b/pkg/apimachinery/apis/common/v0alpha1/unstructured_test.go @@ -0,0 +1,44 @@ +package v0alpha1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDeepCopyJSON(t *testing.T) { + obj := &Unstructured{ + Object: map[string]interface{}{ + "int": int(2), + "int16": int16(2), + "int32": int32(2), + "uint64": uint64(2), + "ref": &ObjectReference{ + Resource: "x", + }, + "array": []any{ + int(1), int64(2), "hello", + }, + "string": "hello", + "bool": true, + "map": map[string]any{ + "x": &ObjectReference{ + Resource: "x", + }, + }, + "object": &v1.APIGroup{ + Name: "HELLO", + }, + }, + } + before, err := json.Marshal(obj) + require.NoError(t, err) + + clone := obj.DeepCopy() + after, err := json.Marshal(clone) + require.NoError(t, err) + + require.JSONEq(t, string(before), string(after)) +}