diff --git a/go.mod b/go.mod index 2b3298c1f..8fd6f053e 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.0 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 - github.com/imdario/mergo v0.3.6 github.com/peterbourgon/diskv v2.0.1+incompatible github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 11caa93f4..9549a3052 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/tools/clientcmd/client_config.go b/tools/clientcmd/client_config.go index a4f748408..cd0a8649b 100644 --- a/tools/clientcmd/client_config.go +++ b/tools/clientcmd/client_config.go @@ -29,8 +29,6 @@ import ( clientauth "k8s.io/client-go/tools/auth" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "k8s.io/klog/v2" - - "github.com/imdario/mergo" ) const ( @@ -241,10 +239,14 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { if err != nil { return nil, err } - mergo.Merge(clientConfig, userAuthPartialConfig, mergo.WithOverride) + if err := merge(clientConfig, userAuthPartialConfig); err != nil { + return nil, err + } serverAuthPartialConfig := getServerIdentificationPartialConfig(configClusterInfo) - mergo.Merge(clientConfig, serverAuthPartialConfig, mergo.WithOverride) + if err := merge(clientConfig, serverAuthPartialConfig); err != nil { + return nil, err + } } return clientConfig, nil @@ -326,8 +328,12 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo) previouslyMergedConfig := mergedConfig mergedConfig = &restclient.Config{} - mergo.Merge(mergedConfig, promptedConfig, mergo.WithOverride) - mergo.Merge(mergedConfig, previouslyMergedConfig, mergo.WithOverride) + if err := merge(mergedConfig, promptedConfig); err != nil { + return nil, err + } + if err := merge(mergedConfig, previouslyMergedConfig); err != nil { + return nil, err + } config.promptedCredentials.username = mergedConfig.Username config.promptedCredentials.password = mergedConfig.Password } @@ -335,7 +341,7 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI return mergedConfig, nil } -// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information +// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged for only user identification information func makeUserIdentificationConfig(info clientauth.Info) *restclient.Config { config := &restclient.Config{} config.Username = info.User @@ -495,12 +501,16 @@ func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) { mergedContext := clientcmdapi.NewContext() if configContext, exists := contexts[contextName]; exists { - mergo.Merge(mergedContext, configContext, mergo.WithOverride) + if err := merge(mergedContext, configContext); err != nil { + return clientcmdapi.Context{}, err + } } else if required { return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName) } if config.overrides != nil { - mergo.Merge(mergedContext, config.overrides.Context, mergo.WithOverride) + if err := merge(mergedContext, &config.overrides.Context); err != nil { + return clientcmdapi.Context{}, err + } } return *mergedContext, nil @@ -513,12 +523,16 @@ func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) { mergedAuthInfo := clientcmdapi.NewAuthInfo() if configAuthInfo, exists := authInfos[authInfoName]; exists { - mergo.Merge(mergedAuthInfo, configAuthInfo, mergo.WithOverride) + if err := merge(mergedAuthInfo, configAuthInfo); err != nil { + return clientcmdapi.AuthInfo{}, err + } } else if required { return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName) } if config.overrides != nil { - mergo.Merge(mergedAuthInfo, config.overrides.AuthInfo, mergo.WithOverride) + if err := merge(mergedAuthInfo, &config.overrides.AuthInfo); err != nil { + return clientcmdapi.AuthInfo{}, err + } } return *mergedAuthInfo, nil @@ -531,15 +545,21 @@ func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) { mergedClusterInfo := clientcmdapi.NewCluster() if config.overrides != nil { - mergo.Merge(mergedClusterInfo, config.overrides.ClusterDefaults, mergo.WithOverride) + if err := merge(mergedClusterInfo, &config.overrides.ClusterDefaults); err != nil { + return clientcmdapi.Cluster{}, err + } } if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { - mergo.Merge(mergedClusterInfo, configClusterInfo, mergo.WithOverride) + if err := merge(mergedClusterInfo, configClusterInfo); err != nil { + return clientcmdapi.Cluster{}, err + } } else if required { return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName) } if config.overrides != nil { - mergo.Merge(mergedClusterInfo, config.overrides.ClusterInfo, mergo.WithOverride) + if err := merge(mergedClusterInfo, &config.overrides.ClusterInfo); err != nil { + return clientcmdapi.Cluster{}, err + } } // * An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data diff --git a/tools/clientcmd/client_config_test.go b/tools/clientcmd/client_config_test.go index 17d1b7b20..4872eadf3 100644 --- a/tools/clientcmd/client_config_test.go +++ b/tools/clientcmd/client_config_test.go @@ -22,16 +22,17 @@ import ( "strings" "testing" - utiltesting "k8s.io/client-go/util/testing" - - "github.com/imdario/mergo" + "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/dump" restclient "k8s.io/client-go/rest" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + utiltesting "k8s.io/client-go/util/testing" + "k8s.io/utils/ptr" ) -func TestMergoSemantics(t *testing.T) { +func TestMergeSemantics(t *testing.T) { type U struct { A string B int64 @@ -41,7 +42,11 @@ func TestMergoSemantics(t *testing.T) { X string Y int64 U U + + StringPtr *string + StructPtr *U } + var testDataStruct = []struct { dst T src T @@ -62,22 +67,31 @@ func TestMergoSemantics(t *testing.T) { src: T{S: []string{"test1", "test2", "test3"}}, expected: T{S: []string{"test1", "test2", "test3"}}, }, + { + dst: T{}, + src: T{StringPtr: ptr.To("a"), StructPtr: ptr.To(U{A: "a", B: 1})}, + expected: T{StringPtr: ptr.To("a"), StructPtr: ptr.To(U{A: "a", B: 1})}, + }, + { + dst: T{StringPtr: ptr.To("a"), StructPtr: ptr.To(U{A: "a", B: 1})}, + src: T{}, + expected: T{StringPtr: ptr.To("a"), StructPtr: ptr.To(U{A: "a", B: 1})}, + }, + { + dst: T{StringPtr: ptr.To("a"), StructPtr: ptr.To(U{A: "a", B: 1})}, + src: T{StringPtr: ptr.To("b"), StructPtr: ptr.To(U{A: "b", B: 0})}, // zero value B overrides non-zero value B in pointer member + expected: T{StringPtr: ptr.To("b"), StructPtr: ptr.To(U{A: "b", B: 0})}, + }, } - for _, data := range testDataStruct { - err := mergo.Merge(&data.dst, &data.src, mergo.WithOverride) + for i, data := range testDataStruct { + t.Logf("case %d: %+v, %+v", i, data.dst, data.src) + err := merge(&data.dst, &data.src) if err != nil { t.Errorf("error while merging: %s", err) } if !reflect.DeepEqual(data.dst, data.expected) { - // The mergo library has previously changed in a an incompatible way. - // example: - // - // https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a - // // This test verifies that the semantics of the merge are what we expect. - // If they are not, the mergo library may have been updated and broken - // unexpectedly. - t.Errorf("mergo.MergeWithOverwrite did not provide expected output: %+v doesn't match %+v", data.dst, data.expected) + t.Errorf("merge did not provide expected output:\ngot: %s\nwant: %s\ndiff: %s", dump.OneLine(data.dst), dump.OneLine(data.expected), cmp.Diff(data.dst, data.expected)) } } @@ -93,22 +107,152 @@ func TestMergoSemantics(t *testing.T) { }, } for _, data := range testDataMap { - err := mergo.Merge(&data.dst, &data.src, mergo.WithOverride) + err := merge(&data.dst, &data.src) if err != nil { t.Errorf("error while merging: %s", err) } if !reflect.DeepEqual(data.dst, data.expected) { - // The mergo library has previously changed in a an incompatible way. - // example: - // - // https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a - // // This test verifies that the semantics of the merge are what we expect. - // If they are not, the mergo library may have been updated and broken - // unexpectedly. - t.Errorf("mergo.MergeWithOverwrite did not provide expected output: %+v doesn't match %+v", data.dst, data.expected) + t.Errorf("merge did not provide expected output: %+v doesn't match %+v", data.dst, data.expected) } } + + type Struct struct { + String string + StringP *string + Int int + IntP *int + Bool bool + BoolP *bool + Slice []string + SliceP *[]string + Map map[string]string + MapP *map[string]string + } + type T2 struct { + String string + StringP *string + Int int + IntP *int + Bool bool + BoolP *bool + Slice []string + SliceP *[]string + Map map[string]string + MapP *map[string]string + Struct Struct + StructP *Struct + } + + var testcases = []struct { + name string + dst T2 + src T2 + expected T2 + }{ + { + name: "zero noop", + dst: T2{}, + src: T2{}, + expected: T2{}, + }, + { + name: "override zero dst", + dst: T2{}, + src: T2{ + String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"}), + Struct: Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}, + StructP: ptr.To(Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}), + }, + expected: T2{ + String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"}), + Struct: Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}, + StructP: ptr.To(Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}), + }, + }, + { + name: "noop zero src", + dst: T2{ + String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"}), + Struct: Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}, + StructP: ptr.To(Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}), + }, + src: T2{}, + expected: T2{ + String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"}), + Struct: Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}, + StructP: ptr.To(Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}), + }, + }, + { + name: "overwrite non-zero values, merge maps", + dst: T2{ + String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), + Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"}), + Struct: Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}, + StructP: ptr.To(Struct{String: "a", StringP: ptr.To("a"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"a"}, SliceP: ptr.To([]string{"a"}), Map: map[string]string{"a": "1"}, MapP: ptr.To(map[string]string{"a": "1"})}), + }, + src: T2{ + String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), + Map: map[string]string{"b": "1"}, MapP: ptr.To(map[string]string{"b": "1"}), + Struct: Struct{String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), Map: map[string]string{"b": "1"}, MapP: ptr.To(map[string]string{"b": "1"})}, + StructP: ptr.To(Struct{String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), Map: map[string]string{"b": "1"}, MapP: ptr.To(map[string]string{"b": "1"})}), + }, + expected: T2{ + // overwritten + String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), MapP: ptr.To(map[string]string{"b": "1"}), + // merged + Map: map[string]string{"a": "1", "b": "1"}, + Struct: Struct{ + // overwritten + String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), MapP: ptr.To(map[string]string{"b": "1"}), + // merged + Map: map[string]string{"a": "1", "b": "1"}, + }, + // overwritten + StructP: ptr.To(Struct{String: "b", StringP: ptr.To("b"), Int: 1, IntP: ptr.To(1), Bool: true, BoolP: ptr.To(true), Slice: []string{"b"}, SliceP: ptr.To([]string{"b"}), Map: map[string]string{"b": "1"}, MapP: ptr.To(map[string]string{"b": "1"})}), + }, + }, + { + name: "overwrite non-zero values, merge maps", + dst: T2{ + Struct: Struct{ + String: "a", Int: 1, Map: map[string]string{"a": "1", "shared": "1"}, MapP: ptr.To(map[string]string{"a": "1", "shared": "1"}), + }, + StructP: ptr.To(Struct{ + String: "a", Int: 1, Map: map[string]string{"a": "1", "shared": "1"}, MapP: ptr.To(map[string]string{"a": "1", "shared": "1"}), + }), + }, + src: T2{ + Struct: Struct{ + String: "b", Int: 0, Map: map[string]string{"b": "1"}, + }, + StructP: ptr.To(Struct{ + String: "b", Int: 0, Map: map[string]string{"b": "1"}, + }), + }, + expected: T2{ + Struct: Struct{ + String: "b", Int: 1 /* preserved */, Map: map[string]string{"a": "1", "b": "1", "shared": "1"} /* merged */, MapP: ptr.To(map[string]string{"a": "1", "shared": "1"}), /* preserved */ + }, + StructP: ptr.To(Struct{ + String: "b", Int: 0 /* overwritten */, Map: map[string]string{"b": "1"} /* unmerged */, MapP: nil, /* overwritten*/ + }), + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + err := merge(&tc.dst, &tc.src) + if err != nil { + t.Errorf("error while merging: %s", err) + } + if !reflect.DeepEqual(tc.dst, tc.expected) { + // This test verifies that the semantics of the merge are what we expect. + t.Errorf("merge did not provide expected output:\ngot: %s\nwant: %s\ndiff: %s", dump.OneLine(tc.dst), dump.OneLine(tc.expected), cmp.Diff(tc.dst, tc.expected)) + } + }) + } } func createValidTestConfig() *clientcmdapi.Config { diff --git a/tools/clientcmd/loader.go b/tools/clientcmd/loader.go index b75737f1c..c900e5fd1 100644 --- a/tools/clientcmd/loader.go +++ b/tools/clientcmd/loader.go @@ -24,7 +24,6 @@ import ( goruntime "runtime" "strings" - "github.com/imdario/mergo" "k8s.io/klog/v2" "k8s.io/apimachinery/pkg/runtime" @@ -248,7 +247,9 @@ func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) { mapConfig := clientcmdapi.NewConfig() for _, kubeconfig := range kubeconfigs { - mergo.Merge(mapConfig, kubeconfig, mergo.WithOverride) + if err := merge(mapConfig, kubeconfig); err != nil { + return nil, err + } } // merge all of the struct values in the reverse order so that priority is given correctly @@ -256,14 +257,20 @@ func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) { nonMapConfig := clientcmdapi.NewConfig() for i := len(kubeconfigs) - 1; i >= 0; i-- { kubeconfig := kubeconfigs[i] - mergo.Merge(nonMapConfig, kubeconfig, mergo.WithOverride) + if err := merge(nonMapConfig, kubeconfig); err != nil { + return nil, err + } } // since values are overwritten, but maps values are not, we can merge the non-map config on top of the map config and // get the values we expect. config := clientcmdapi.NewConfig() - mergo.Merge(config, mapConfig, mergo.WithOverride) - mergo.Merge(config, nonMapConfig, mergo.WithOverride) + if err := merge(config, mapConfig); err != nil { + return nil, err + } + if err := merge(config, nonMapConfig); err != nil { + return nil, err + } if rules.ResolvePaths() { if err := ResolveLocalPaths(config); err != nil { diff --git a/tools/clientcmd/merge.go b/tools/clientcmd/merge.go new file mode 100644 index 000000000..3d74e6029 --- /dev/null +++ b/tools/clientcmd/merge.go @@ -0,0 +1,121 @@ +/* +Copyright 2024 The Kubernetes 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 clientcmd + +import ( + "fmt" + "reflect" + "strings" +) + +// recursively merges src into dst: +// - non-pointer struct fields with any exported fields are recursively merged +// - non-pointer struct fields with only unexported fields prefer src if the field is non-zero +// - maps are shallow merged with src keys taking priority over dst +// - non-zero src fields encountered during recursion that are not maps or structs overwrite and recursion stops +func merge[T any](dst, src *T) error { + if dst == nil { + return fmt.Errorf("cannot merge into nil pointer") + } + if src == nil { + return nil + } + return mergeValues(nil, reflect.ValueOf(dst).Elem(), reflect.ValueOf(src).Elem()) +} + +func mergeValues(fieldNames []string, dst, src reflect.Value) error { + dstType := dst.Type() + // no-op if we can't read the src + if !src.IsValid() { + return nil + } + // sanity check types match + if srcType := src.Type(); dstType != srcType { + return fmt.Errorf("cannot merge mismatched types (%s, %s) at %s", dstType, srcType, strings.Join(fieldNames, ".")) + } + + switch dstType.Kind() { + case reflect.Struct: + if hasExportedField(dstType) { + // recursively merge + for i, n := 0, dstType.NumField(); i < n; i++ { + if err := mergeValues(append(fieldNames, dstType.Field(i).Name), dst.Field(i), src.Field(i)); err != nil { + return err + } + } + } else if dst.CanSet() { + // If all fields are unexported, overwrite with src. + // Using src.IsZero() would make more sense but that's not what mergo did. + dst.Set(src) + } + + case reflect.Map: + if dst.CanSet() && !src.IsZero() { + // initialize dst if needed + if dst.IsZero() { + dst.Set(reflect.MakeMap(dstType)) + } + // shallow-merge overwriting dst keys with src keys + for _, mapKey := range src.MapKeys() { + dst.SetMapIndex(mapKey, src.MapIndex(mapKey)) + } + } + + case reflect.Slice: + if dst.CanSet() && src.Len() > 0 { + // overwrite dst with non-empty src slice + dst.Set(src) + } + + case reflect.Pointer: + if dst.CanSet() && !src.IsZero() { + // overwrite dst with non-zero values for other types + if dstType.Elem().Kind() == reflect.Struct { + // use struct pointer as-is + dst.Set(src) + } else { + // shallow-copy non-struct pointer (interfaces, primitives, etc) + dst.Set(reflect.New(dstType.Elem())) + dst.Elem().Set(src.Elem()) + } + } + + default: + if dst.CanSet() && !src.IsZero() { + // overwrite dst with non-zero values for other types + dst.Set(src) + } + } + + return nil +} + +// hasExportedField returns true if the given type has any exported fields, +// or if it has any anonymous/embedded struct fields with exported fields +func hasExportedField(dstType reflect.Type) bool { + for i, n := 0, dstType.NumField(); i < n; i++ { + field := dstType.Field(i) + if field.Anonymous && field.Type.Kind() == reflect.Struct { + if hasExportedField(dstType.Field(i).Type) { + return true + } + } else if len(field.PkgPath) == 0 { + return true + } + } + return false +}