From bee9c1a108e516922ef4a5cdb3365ff8c3b6b7cf Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 25 Sep 2025 09:29:57 -0600 Subject: [PATCH] chore: replace github.com/mitchellh/copystructure Signed-off-by: Terry Howe --- go.mod | 2 +- internal/chart/v3/util/dependencies.go | 3 +- internal/copystructure/copystructure.go | 120 ++++++ internal/copystructure/copystructure_test.go | 374 +++++++++++++++++++ pkg/chart/common/util/coalesce.go | 3 +- pkg/chart/v2/util/dependencies.go | 3 +- 6 files changed, 498 insertions(+), 7 deletions(-) create mode 100644 internal/copystructure/copystructure.go create mode 100644 internal/copystructure/copystructure_test.go diff --git a/go.mod b/go.mod index c08dabc3b..0d4b704be 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 github.com/mattn/go-shellwords v1.0.12 - github.com/mitchellh/copystructure v1.2.0 github.com/moby/term v0.5.2 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 @@ -112,6 +111,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/miekg/dns v1.1.57 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/spdystream v0.5.0 // indirect diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index 489772115..cd7a8b78c 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -20,9 +20,8 @@ import ( "log/slog" "strings" - "github.com/mitchellh/copystructure" - chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/copystructure" "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common/util" ) diff --git a/internal/copystructure/copystructure.go b/internal/copystructure/copystructure.go new file mode 100644 index 000000000..d226975e7 --- /dev/null +++ b/internal/copystructure/copystructure.go @@ -0,0 +1,120 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package copystructure + +import ( + "fmt" + "reflect" +) + +// Copy performs a deep copy of the given interface{}. +// This implementation handles the specific use cases needed by Helm. +func Copy(src interface{}) (interface{}, error) { + if src == nil { + return make(map[string]interface{}), nil + } + return copyValue(reflect.ValueOf(src)) +} + +// copyValue handles copying using reflection for non-map types +func copyValue(original reflect.Value) (interface{}, error) { + switch original.Kind() { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, + reflect.Complex64, reflect.Complex128, reflect.String, reflect.Array: + return original.Interface(), nil + + case reflect.Interface: + if original.IsNil() { + return original.Interface(), nil + } + return copyValue(original.Elem()) + + case reflect.Map: + if original.IsNil() { + return original.Interface(), nil + } + copied := reflect.MakeMap(original.Type()) + + var err error + var child interface{} + iter := original.MapRange() + for iter.Next() { + key := iter.Key() + value := iter.Value() + + if value.Kind() == reflect.Interface && value.IsNil() { + copied.SetMapIndex(key, value) + continue + } + + child, err = copyValue(value) + if err != nil { + return nil, err + } + copied.SetMapIndex(key, reflect.ValueOf(child)) + } + return copied.Interface(), nil + + case reflect.Pointer: + if original.IsNil() { + return original.Interface(), nil + } + copied, err := copyValue(original.Elem()) + if err != nil { + return nil, err + } + ptr := reflect.New(original.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(copied)) + return ptr.Interface(), nil + + case reflect.Slice: + if original.IsNil() { + return original.Interface(), nil + } + copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap()) + for i := 0; i < original.Len(); i++ { + val, err := copyValue(original.Index(i)) + if err != nil { + return nil, err + } + copied.Index(i).Set(reflect.ValueOf(val)) + } + return copied.Interface(), nil + + case reflect.Struct: + copied := reflect.New(original.Type()).Elem() + for i := 0; i < original.NumField(); i++ { + elem, err := copyValue(original.Field(i)) + if err != nil { + return nil, err + } + copied.Field(i).Set(reflect.ValueOf(elem)) + } + return copied.Interface(), nil + + case reflect.Func, reflect.Chan, reflect.UnsafePointer: + if original.IsNil() { + return original.Interface(), nil + } + return original.Interface(), nil + + default: + return original.Interface(), fmt.Errorf("unsupported type %v", original) + } +} diff --git a/internal/copystructure/copystructure_test.go b/internal/copystructure/copystructure_test.go new file mode 100644 index 000000000..90678dfc1 --- /dev/null +++ b/internal/copystructure/copystructure_test.go @@ -0,0 +1,374 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package copystructure + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopy_Nil(t *testing.T) { + result, err := Copy(nil) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{}, result) +} + +func TestCopy_PrimitiveTypes(t *testing.T) { + tests := []struct { + name string + input interface{} + }{ + {"bool", true}, + {"int", 42}, + {"int8", int8(8)}, + {"int16", int16(16)}, + {"int32", int32(32)}, + {"int64", int64(64)}, + {"uint", uint(42)}, + {"uint8", uint8(8)}, + {"uint16", uint16(16)}, + {"uint32", uint32(32)}, + {"uint64", uint64(64)}, + {"float32", float32(3.14)}, + {"float64", 3.14159}, + {"complex64", complex64(1 + 2i)}, + {"complex128", 1 + 2i}, + {"string", "hello world"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Copy(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.input, result) + }) + } +} + +func TestCopy_Array(t *testing.T) { + input := [3]int{1, 2, 3} + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) +} + +func TestCopy_Slice(t *testing.T) { + t.Run("slice of ints", func(t *testing.T) { + input := []int{1, 2, 3, 4, 5} + result, err := Copy(input) + require.NoError(t, err) + + resultSlice, ok := result.([]int) + require.True(t, ok) + assert.Equal(t, input, resultSlice) + + // Verify it's a deep copy by modifying original + input[0] = 999 + assert.Equal(t, 1, resultSlice[0]) + }) + + t.Run("slice of strings", func(t *testing.T) { + input := []string{"a", "b", "c"} + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil slice", func(t *testing.T) { + var input []int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("slice of maps", func(t *testing.T) { + input := []map[string]interface{}{ + {"key1": "value1"}, + {"key2": "value2"}, + } + result, err := Copy(input) + require.NoError(t, err) + + resultSlice, ok := result.([]map[string]interface{}) + require.True(t, ok) + assert.Equal(t, input, resultSlice) + + // Verify deep copy + input[0]["key1"] = "modified" + assert.Equal(t, "value1", resultSlice[0]["key1"]) + }) +} + +func TestCopy_Map(t *testing.T) { + t.Run("map[string]interface{}", func(t *testing.T) { + input := map[string]interface{}{ + "string": "value", + "int": 42, + "bool": true, + "nested": map[string]interface{}{ + "inner": "value", + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, input, resultMap) + + // Verify deep copy + input["string"] = "modified" + assert.Equal(t, "value", resultMap["string"]) + + nestedInput := input["nested"].(map[string]interface{}) + nestedResult := resultMap["nested"].(map[string]interface{}) + nestedInput["inner"] = "modified" + assert.Equal(t, "value", nestedResult["inner"]) + }) + + t.Run("map[string]string", func(t *testing.T) { + input := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil map", func(t *testing.T) { + var input map[string]interface{} + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("map with nil values", func(t *testing.T) { + input := map[string]interface{}{ + "key1": "value1", + "key2": nil, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, input, resultMap) + assert.Nil(t, resultMap["key2"]) + }) +} + +func TestCopy_Struct(t *testing.T) { + type TestStruct struct { + Name string + Age int + Active bool + Scores []int + Metadata map[string]interface{} + } + + input := TestStruct{ + Name: "John", + Age: 30, + Active: true, + Scores: []int{95, 87, 92}, + Metadata: map[string]interface{}{ + "level": "advanced", + "tags": []string{"go", "programming"}, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultStruct, ok := result.(TestStruct) + require.True(t, ok) + assert.Equal(t, input, resultStruct) + + // Verify deep copy + input.Name = "Modified" + input.Scores[0] = 999 + assert.Equal(t, "John", resultStruct.Name) + assert.Equal(t, 95, resultStruct.Scores[0]) +} + +func TestCopy_Pointer(t *testing.T) { + t.Run("pointer to int", func(t *testing.T) { + value := 42 + input := &value + + result, err := Copy(input) + require.NoError(t, err) + + resultPtr, ok := result.(*int) + require.True(t, ok) + assert.Equal(t, *input, *resultPtr) + + // Verify they point to different memory locations + assert.NotSame(t, input, resultPtr) + + // Verify deep copy + *input = 999 + assert.Equal(t, 42, *resultPtr) + }) + + t.Run("pointer to struct", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + input := &Person{Name: "Alice", Age: 25} + + result, err := Copy(input) + require.NoError(t, err) + + resultPtr, ok := result.(*Person) + require.True(t, ok) + assert.Equal(t, *input, *resultPtr) + assert.NotSame(t, input, resultPtr) + }) + + t.Run("nil pointer", func(t *testing.T) { + var input *int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} + +func TestCopy_Interface(t *testing.T) { + t.Run("interface{} with value", func(t *testing.T) { + var input interface{} = "hello" + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil interface{}", func(t *testing.T) { + var input interface{} + result, err := Copy(input) + require.NoError(t, err) + // Copy(nil) returns an empty map according to the implementation + assert.Equal(t, map[string]interface{}{}, result) + }) + + t.Run("interface{} with complex value", func(t *testing.T) { + var input interface{} = map[string]interface{}{ + "key": "value", + "nested": map[string]interface{}{ + "inner": 42, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) +} + +func TestCopy_ComplexNested(t *testing.T) { + input := map[string]interface{}{ + "users": []map[string]interface{}{ + { + "name": "Alice", + "age": 30, + "addresses": []map[string]interface{}{ + {"type": "home", "city": "NYC"}, + {"type": "work", "city": "SF"}, + }, + }, + { + "name": "Bob", + "age": 25, + "addresses": []map[string]interface{}{ + {"type": "home", "city": "LA"}, + }, + }, + }, + "metadata": map[string]interface{}{ + "version": "1.0", + "flags": []bool{true, false, true}, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]interface{}) + require.True(t, ok) + assert.Equal(t, input, resultMap) + + // Verify deep copy by modifying nested values + users := input["users"].([]map[string]interface{}) + addresses := users[0]["addresses"].([]map[string]interface{}) + addresses[0]["city"] = "Modified" + + resultUsers := resultMap["users"].([]map[string]interface{}) + resultAddresses := resultUsers[0]["addresses"].([]map[string]interface{}) + assert.Equal(t, "NYC", resultAddresses[0]["city"]) +} + +func TestCopy_Functions(t *testing.T) { + t.Run("function", func(t *testing.T) { + input := func() string { return "hello" } + result, err := Copy(input) + require.NoError(t, err) + + // Functions should be copied as-is (same reference) + resultFunc, ok := result.(func() string) + require.True(t, ok) + assert.Equal(t, input(), resultFunc()) + }) + + t.Run("nil function", func(t *testing.T) { + var input func() + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} + +func TestCopy_Channels(t *testing.T) { + t.Run("channel", func(t *testing.T) { + input := make(chan int, 1) + input <- 42 + + result, err := Copy(input) + require.NoError(t, err) + + // Channels should be copied as-is (same reference) + resultChan, ok := result.(chan int) + require.True(t, ok) + + // Since channels are copied as references, verify we can read from the result channel + value := <-resultChan + assert.Equal(t, 42, value) + }) + + t.Run("nil channel", func(t *testing.T) { + var input chan int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} diff --git a/pkg/chart/common/util/coalesce.go b/pkg/chart/common/util/coalesce.go index 5bfa1c608..07794a04a 100644 --- a/pkg/chart/common/util/coalesce.go +++ b/pkg/chart/common/util/coalesce.go @@ -21,8 +21,7 @@ import ( "log" "maps" - "github.com/mitchellh/copystructure" - + "helm.sh/helm/v4/internal/copystructure" chart "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/common" ) diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index a52f09f82..294b782f8 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -20,8 +20,7 @@ import ( "log/slog" "strings" - "github.com/mitchellh/copystructure" - + "helm.sh/helm/v4/internal/copystructure" "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2"