Skip to content

Commit 8afa426

Browse files
authored
[confmap] Fix handling for slices with default values (#12662)
#### Description #11882 introduced issues with handling slices which have default values. It's likely that the removal of `zeroSliceHookFunc` is causing problems with overriding default lists.
1 parent 8bc15fd commit 8afa426

File tree

8 files changed

+62
-62
lines changed

8 files changed

+62
-62
lines changed

.chloggen/revert-confmap-nil.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: bug_fix
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: confmap
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Ensure slices with defaults containing struct values are correctly set.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [12661]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: This reverts the changes made in https://github.com/open-telemetry/opentelemetry-collector/pull/11882.
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

confmap/confmap.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,6 @@ func sanitizeExpanded(a any, useOriginal bool) any {
141141
return c
142142
case []any:
143143
var newSlice []any
144-
if m == nil {
145-
return newSlice
146-
}
147-
newSlice = []any{}
148144
for _, e := range m {
149145
newSlice = append(newSlice, sanitizeExpanded(e, useOriginal))
150146
}
@@ -240,6 +236,7 @@ func decodeConfig(m *Conf, result any, errorUnused bool, skipTopLevelUnmarshaler
240236
// after the main unmarshaler hook is called,
241237
// we unmarshal the embedded structs if present to merge with the result:
242238
unmarshalerEmbeddedStructsHookFunc(),
239+
zeroSliceHookFunc(),
243240
),
244241
}
245242
decoder, err := mapstructure.NewDecoder(dc)
@@ -534,6 +531,37 @@ type Marshaler interface {
534531
Marshal(component *Conf) error
535532
}
536533

534+
// This hook is used to solve the issue: https://github.com/open-telemetry/opentelemetry-collector/issues/4001
535+
// We adopt the suggestion provided in this issue: https://github.com/mitchellh/mapstructure/issues/74#issuecomment-279886492
536+
// We should empty every slice before unmarshalling unless user provided slice is nil.
537+
// Assume that we had a struct with a field of type slice called `keys`, which has default values of ["a", "b"]
538+
//
539+
// type Config struct {
540+
// Keys []string `mapstructure:"keys"`
541+
// }
542+
//
543+
// The configuration provided by users may have following cases
544+
// 1. configuration have `keys` field and have a non-nil values for this key, the output should be overridden
545+
// - for example, input is {"keys", ["c"]}, then output is Config{ Keys: ["c"]}
546+
//
547+
// 2. configuration have `keys` field and have an empty slice for this key, the output should be overridden by empty slices
548+
// - for example, input is {"keys", []}, then output is Config{ Keys: []}
549+
//
550+
// 3. configuration have `keys` field and have nil value for this key, the output should be default config
551+
// - for example, input is {"keys": nil}, then output is Config{ Keys: ["a", "b"]}
552+
//
553+
// 4. configuration have no `keys` field specified, the output should be default config
554+
// - for example, input is {}, then output is Config{ Keys: ["a", "b"]}
555+
func zeroSliceHookFunc() mapstructure.DecodeHookFuncValue {
556+
return func(from reflect.Value, to reflect.Value) (any, error) {
557+
if to.CanSet() && to.Kind() == reflect.Slice && from.Kind() == reflect.Slice {
558+
to.Set(reflect.MakeSlice(to.Type(), from.Len(), from.Cap()))
559+
}
560+
561+
return from.Interface(), nil
562+
}
563+
}
564+
537565
type moduleFactory[T any, S any] interface {
538566
Create(s S) T
539567
}

confmap/confmap_test.go

-44
Original file line numberDiff line numberDiff line change
@@ -689,50 +689,6 @@ func TestZeroSliceHookFunc(t *testing.T) {
689689
}
690690
}
691691

692-
func TestNilValuesUnchanged(t *testing.T) {
693-
type structWithSlices struct {
694-
Strings []string `mapstructure:"strings"`
695-
}
696-
697-
slicesStruct := &structWithSlices{}
698-
699-
nilCfg := map[string]any{
700-
"strings": []any(nil),
701-
}
702-
nilConf := NewFromStringMap(nilCfg)
703-
err := nilConf.Unmarshal(slicesStruct)
704-
require.NoError(t, err)
705-
706-
confFromStruct := New()
707-
err = confFromStruct.Marshal(slicesStruct)
708-
require.NoError(t, err)
709-
710-
require.Equal(t, nilCfg, nilConf.ToStringMap())
711-
require.EqualValues(t, nilConf.ToStringMap(), confFromStruct.ToStringMap())
712-
}
713-
714-
func TestEmptySliceUnchanged(t *testing.T) {
715-
type structWithSlices struct {
716-
Strings []string `mapstructure:"strings"`
717-
}
718-
719-
slicesStruct := &structWithSlices{}
720-
721-
nilCfg := map[string]any{
722-
"strings": []any{},
723-
}
724-
nilConf := NewFromStringMap(nilCfg)
725-
err := nilConf.Unmarshal(slicesStruct)
726-
require.NoError(t, err)
727-
728-
confFromStruct := New()
729-
err = confFromStruct.Marshal(slicesStruct)
730-
require.NoError(t, err)
731-
732-
require.Equal(t, nilCfg, nilConf.ToStringMap())
733-
require.EqualValues(t, nilConf.ToStringMap(), confFromStruct.ToStringMap())
734-
}
735-
736692
type C struct {
737693
Modifiers []string `mapstructure:"modifiers"`
738694
}

confmap/confmaptest/configtest_test.go

+3-8
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,11 @@ func TestLoadConf(t *testing.T) {
3030
assert.Equal(t, map[string]any{"floating": 3.14}, cfg.ToStringMap())
3131
}
3232

33-
func TestToStringMapSanitizeNil(t *testing.T) {
34-
cfg, err := LoadConf(filepath.Join("testdata", "nil.yaml"))
35-
require.NoError(t, err)
36-
assert.Equal(t, map[string]any{"slice": nil}, cfg.ToStringMap())
37-
}
38-
39-
func TestToStringMapEmptySlice(t *testing.T) {
33+
func TestToStringMapSanitizeEmptySlice(t *testing.T) {
4034
cfg, err := LoadConf(filepath.Join("testdata", "empty-slice.yaml"))
4135
require.NoError(t, err)
42-
assert.Equal(t, map[string]any{"slice": []any{}}, cfg.ToStringMap())
36+
var nilSlice []any
37+
assert.Equal(t, map[string]any{"slice": nilSlice}, cfg.ToStringMap())
4338
}
4439

4540
func TestValidateProviderScheme(t *testing.T) {
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
slice: []
1+
slice: [] # empty slices are sanitized to nil in ToStringMap

confmap/confmaptest/testdata/nil.yaml

-1
This file was deleted.

confmap/internal/mapstructure/encoder.go

-3
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ func (e *Encoder) encodeSlice(value reflect.Value) (any, error) {
141141
Kind: value.Kind(),
142142
}
143143
}
144-
if value.IsNil() {
145-
return []any(nil), nil
146-
}
147144
result := make([]any, value.Len())
148145
for i := 0; i < value.Len(); i++ {
149146
var err error

service/config_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func TestConfmapMarshalConfig(t *testing.T) {
124124
"host": "localhost",
125125
"port": 8888,
126126
"with_resource_constant_labels": map[string]any{
127-
"included": []any{},
127+
"included": []any(nil),
128128
},
129129
"without_scope_info": true,
130130
"without_type_suffix": true,

0 commit comments

Comments
 (0)