Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[config] Deprecate config.UnmarshalConfig #9750

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions .chloggen/embedded_struct_unmarshaler.yaml
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: breaking

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: component

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Deprecate `component.UnmarshalConfig`, use `conf.Unmarshal(&intoCfg)` instead.

# One or more tracking issues or pull requests related to the change
issues: [7102]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
7 changes: 1 addition & 6 deletions component/config.go
Expand Up @@ -27,13 +27,8 @@ type Config any
var configValidatorType = reflect.TypeOf((*ConfigValidator)(nil)).Elem()

// UnmarshalConfig helper function to UnmarshalConfig a Config.
// It checks if the config implements confmap.Unmarshaler and uses that if available,
// otherwise uses Map.UnmarshalExact, erroring if a field is nonexistent.
// Deprecated: [v0.99.0] Use conf.Unmarshal(&intoCfg)
func UnmarshalConfig(conf *confmap.Conf, intoCfg Config) error {
if cu, ok := intoCfg.(confmap.Unmarshaler); ok {
return cu.Unmarshal(conf)
}

return conf.Unmarshal(intoCfg)
}

Expand Down
11 changes: 5 additions & 6 deletions component/config_test.go
Expand Up @@ -424,19 +424,18 @@ func TestNewType(t *testing.T) {
}

type configWithEmbeddedStruct struct {
String string `mapstructure:"string"`
Num int `mapstructure:"num"`
embeddedUnmarshallingConfig
String string `mapstructure:"string"`
Num int `mapstructure:"num"`
EmbeddedUnmarshallingConfig `mapstructure:",squash"`
}

type embeddedUnmarshallingConfig struct {
type EmbeddedUnmarshallingConfig struct {
}

func (euc *embeddedUnmarshallingConfig) Unmarshal(_ *confmap.Conf) error {
func (euc *EmbeddedUnmarshallingConfig) Unmarshal(_ *confmap.Conf) error {
return nil // do nothing.
}
func TestStructWithEmbeddedUnmarshaling(t *testing.T) {
atoulme marked this conversation as resolved.
Show resolved Hide resolved
t.Skip("Skipping, to be fixed with https://github.com/open-telemetry/opentelemetry-collector/issues/7102")
cfgMap := confmap.NewFromStringMap(map[string]any{
"string": "foo",
"num": 123,
Expand Down
22 changes: 13 additions & 9 deletions confmap/confmap.go
Expand Up @@ -40,7 +40,8 @@ func NewFromStringMap(data map[string]any) *Conf {
// Conf represents the raw configuration map for the OpenTelemetry Collector.
// The confmap.Conf can be unmarshalled into the Collector's config using the "service" package.
type Conf struct {
k *koanf.Koanf
k *koanf.Koanf
skipTopLevelUnmarshaler bool
}

// AllKeys returns all keys holding a value, regardless of where they are set.
Expand Down Expand Up @@ -79,7 +80,7 @@ func (l *Conf) Unmarshal(result any, opts ...UnmarshalOption) error {
for _, opt := range opts {
opt.apply(&set)
}
return decodeConfig(l, result, !set.ignoreUnused)
return decodeConfig(l, result, !set.ignoreUnused, l.skipTopLevelUnmarshaler)
}

type marshalOption struct{}
Expand Down Expand Up @@ -146,7 +147,7 @@ func (l *Conf) ToStringMap() map[string]any {
// uniqueness of component IDs (see mapKeyStringToMapKeyTextUnmarshalerHookFunc).
// Decodes time.Duration from strings. Allows custom unmarshaling for structs implementing
// encoding.TextUnmarshaler. Allows custom unmarshaling for structs implementing confmap.Unmarshaler.
func decodeConfig(m *Conf, result any, errorUnused bool) error {
func decodeConfig(m *Conf, result any, errorUnused bool, skipTopLevelUnmarshaler bool) error {
dc := &mapstructure.DecoderConfig{
ErrorUnused: errorUnused,
Result: result,
Expand All @@ -159,12 +160,11 @@ func decodeConfig(m *Conf, result any, errorUnused bool) error {
mapKeyStringToMapKeyTextUnmarshalerHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.TextUnmarshallerHookFunc(),
unmarshalerHookFunc(result),
unmarshalerHookFunc(result, skipTopLevelUnmarshaler),
// after the main unmarshaler hook is called,
// we unmarshal the embedded structs if present to merge with the result:
unmarshalerEmbeddedStructsHookFunc(),
zeroSliceHookFunc(),
negativeUintHookFunc(),
),
}
decoder, err := mapstructure.NewDecoder(dc)
Expand Down Expand Up @@ -290,7 +290,9 @@ func unmarshalerEmbeddedStructsHookFunc() mapstructure.DecodeHookFuncValue {
f := to.Type().Field(i)
if f.IsExported() && slices.Contains(strings.Split(f.Tag.Get("mapstructure"), ","), "squash") {
if unmarshaler, ok := to.Field(i).Addr().Interface().(Unmarshaler); ok {
if err := unmarshaler.Unmarshal(NewFromStringMap(fromAsMap)); err != nil {
c := NewFromStringMap(fromAsMap)
c.skipTopLevelUnmarshaler = true
if err := unmarshaler.Unmarshal(c); err != nil {
return nil, err
}
// the struct we receive from this unmarshaling only contains fields related to the embedded struct.
Expand All @@ -313,15 +315,15 @@ func unmarshalerEmbeddedStructsHookFunc() mapstructure.DecodeHookFuncValue {

// Provides a mechanism for individual structs to define their own unmarshal logic,
// by implementing the Unmarshaler interface.
func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue {
func unmarshalerHookFunc(result any, skipTopLevelUnmarshaler bool) mapstructure.DecodeHookFuncValue {
return func(from reflect.Value, to reflect.Value) (any, error) {
if !to.CanAddr() {
return from.Interface(), nil
}

toPtr := to.Addr().Interface()
// Need to ignore the top structure to avoid circular dependency.
if toPtr == result {
if toPtr == result && skipTopLevelUnmarshaler {
return from.Interface(), nil
}

Expand All @@ -339,7 +341,9 @@ func unmarshalerHookFunc(result any) mapstructure.DecodeHookFuncValue {
unmarshaler = reflect.New(to.Type()).Interface().(Unmarshaler)
}

if err := unmarshaler.Unmarshal(NewFromStringMap(from.Interface().(map[string]any))); err != nil {
c := NewFromStringMap(from.Interface().(map[string]any))
c.skipTopLevelUnmarshaler = true
if err := unmarshaler.Unmarshal(c); err != nil {
return nil, err
}

Expand Down
55 changes: 51 additions & 4 deletions confmap/confmap_test.go
Expand Up @@ -572,10 +572,6 @@ type testErrConfig struct {
Err errConfig `mapstructure:"err"`
}

func (tc *testErrConfig) Unmarshal(component *Conf) error {
return component.Unmarshal(tc)
}

type errConfig struct {
Foo string `mapstructure:"foo"`
}
Expand Down Expand Up @@ -827,3 +823,54 @@ func TestUnmarshalOwnThroughEmbeddedSquashedStruct(t *testing.T) {
require.Equal(t, "success", cfg.Cfg.EmbeddedStructWithUnmarshal.success)
require.Equal(t, "bar", cfg.Cfg.EmbeddedStructWithUnmarshal.Foo)
}

type configWithEmbeddedStruct struct {
String string `mapstructure:"string"`
Num int `mapstructure:"num"`
EmbeddedUnmarshallingConfig `mapstructure:",squash"`
}

type EmbeddedUnmarshallingConfig struct {
Embedded string `mapstructure:"embedded"`
}

func (euc *EmbeddedUnmarshallingConfig) Unmarshal(conf *Conf) error {
if err := conf.Unmarshal(euc, WithIgnoreUnused()); err != nil {
return err
}
euc.Embedded += " unmarshaled"
return nil
}
func TestStructWithEmbeddedUnmarshaling(t *testing.T) {
cfgMap := NewFromStringMap(map[string]any{
"string": "foo",
"num": 123,
"embedded": "bar",
})
tc := &configWithEmbeddedStruct{}
err := cfgMap.Unmarshal(tc)
require.NoError(t, err)
assert.Equal(t, "foo", tc.String)
assert.Equal(t, 123, tc.Num)
assert.Equal(t, "bar unmarshaled", tc.Embedded)
}

type configWithEmbeddedUnmarshalMethod struct {
EmbeddedUnmarshallingMethodConfig
}

type EmbeddedUnmarshallingMethodConfig struct {
MyValue string
}

func (euc *EmbeddedUnmarshallingMethodConfig) Unmarshal(_ *Conf) error {
euc.MyValue = "my custom value"
return nil
}
func TestStructWithEmbeddedUnmarshalMethod(t *testing.T) {
cfgMap := NewFromStringMap(map[string]any{})
tc := &configWithEmbeddedUnmarshalMethod{}
err := cfgMap.Unmarshal(tc, WithIgnoreUnused())
require.NoError(t, err)
assert.Equal(t, "my custom value", tc.MyValue)
}