Skip to content

Commit f594326

Browse files
EdgeN8vdmathieuVihasMakwanamx-psi
authored
[otelcol] Fix missing defaults in unredacted print-config (#14752)
**Description** Fixes an issue where `otelcol print-config --mode=unredacted` omits default values, causing its output structure to differ significantly from `redacted` mode. *Implementation details:* This PR introduces a new `confmap.WithUnredacted()` `MarshalOption` and an `unredactedStringer` interface within the `mapstructure` encoder. This allows the configuration to be marshaled natively without redaction, preserving all default values while cleanly decoupling the encoder from `configopaque`. The previous recursive post-processing workaround has been entirely removed. **Link to tracking issue** Fixes #14750 **Testing** * Removed obsolete tests related to the recursive workaround. * Updated `TestPrintCommand` in `command_print_test.go` to assert default values are correctly handled natively in `unredacted` mode. * Passed `make test` locally. **Documentation** * Added `.chloggen` entry. --------- Signed-off-by: EdgeN8v <ckx19921555595@gmail.com> Co-authored-by: Damien Mathieu <42@dmathieu.com> Co-authored-by: Vihas Makwana <121151420+VihasMakwana@users.noreply.github.com> Co-authored-by: Pablo Baeyens <pablo.baeyens@datadoghq.com>
1 parent 76ede07 commit f594326

10 files changed

Lines changed: 131 additions & 43 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
change_type: bug_fix
2+
component: pkg/otelcol
3+
note: Fix missing default values in unredacted print-config command by introducing confmap.WithUnredacted MarshalOption.
4+
subtext: |
5+
Resolves an issue where the unredacted mode output omitted all default-valued options. By introducing a new MarshalOption to disable redaction directly at the confmap encoding level, the unredacted mode now preserves all component defaults natively without requiring post-processing.
6+
issues: [14750]
7+
change_logs: [user]

confmap/internal/encoder.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@ import (
1414
// EncoderConfig returns a default encoder.EncoderConfig that includes
1515
// an EncodeHook that handles both TextMarshaler and Marshaler
1616
// interfaces.
17-
func EncoderConfig(rawVal any, _ MarshalOptions) *encoder.EncoderConfig {
17+
func EncoderConfig(rawVal any, opts MarshalOptions) *encoder.EncoderConfig {
18+
hooks := []mapstructure.DecodeHookFunc{
19+
encoder.YamlMarshalerHookFunc(),
20+
}
21+
22+
if opts.OpaqueUnredacted {
23+
hooks = append(hooks, encoder.StringTextUnredactedHookFunc())
24+
}
25+
26+
hooks = append(hooks,
27+
encoder.TextMarshalerHookFunc(),
28+
marshalerHookFunc(rawVal),
29+
)
30+
1831
return &encoder.EncoderConfig{
19-
EncodeHook: mapstructure.ComposeDecodeHookFunc(
20-
encoder.YamlMarshalerHookFunc(),
21-
encoder.TextMarshalerHookFunc(),
22-
marshalerHookFunc(rawVal),
23-
),
32+
EncodeHook: mapstructure.ComposeDecodeHookFunc(hooks...),
2433
}
2534
}
2635

confmap/internal/encoder_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package internal // import "go.opentelemetry.io/collector/confmap/internal"
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestEncoderConfigUnredacted(t *testing.T) {
13+
opts := MarshalOptions{OpaqueUnredacted: true}
14+
cfg := EncoderConfig(nil, opts)
15+
require.NotNil(t, cfg)
16+
}

confmap/internal/mapstructure/encoder.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,19 @@ func YamlMarshalerHookFunc() mapstructure.DecodeHookFuncValue {
268268
return from.Interface(), nil
269269
}
270270
}
271+
272+
// StringTextUnredactedHookFunc returns a DecodeHookFuncValue that extracts the underlying
273+
// unredacted string from custom string types that implement encoding.TextMarshaler.
274+
func StringTextUnredactedHookFunc() mapstructure.DecodeHookFuncValue {
275+
return func(from, _ reflect.Value) (any, error) {
276+
// If the underlying kind is a string, and it implements TextMarshaler
277+
// (which obfuscated types like configopaque.String usually do).
278+
if from.Kind() == reflect.String {
279+
if _, ok := from.Interface().(encoding.TextMarshaler); ok {
280+
// Extract the raw string via reflection, bypassing any custom methods.
281+
return from.String(), nil
282+
}
283+
}
284+
return from.Interface(), nil
285+
}
286+
}

confmap/internal/mapstructure/encoder_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/stretchr/testify/require"
1515
)
1616

17+
type obfuscatedTestString string
18+
1719
type TestComplexStruct struct {
1820
Skipped TestEmptyStruct `mapstructure:",squash"`
1921
Nested TestSimpleStruct `mapstructure:",squash"`
@@ -392,3 +394,24 @@ func testHookFunc() mapstructure.DecodeHookFuncValue {
392394
return got.skipped, nil
393395
}
394396
}
397+
398+
func (m obfuscatedTestString) MarshalText() ([]byte, error) {
399+
return []byte("[REDACTED]"), nil
400+
}
401+
402+
func TestStringTextUnredactedHookFunc(t *testing.T) {
403+
hook := StringTextUnredactedHookFunc()
404+
405+
val, err := hook(reflect.ValueOf(123), reflect.Value{})
406+
require.NoError(t, err)
407+
require.Equal(t, 123, val)
408+
409+
val, err = hook(reflect.ValueOf("plain"), reflect.Value{})
410+
require.NoError(t, err)
411+
require.Equal(t, "plain", val)
412+
413+
testSecret := obfuscatedTestString("secret")
414+
val, err = hook(reflect.ValueOf(testSecret), reflect.Value{})
415+
require.NoError(t, err)
416+
require.Equal(t, "secret", val)
417+
}

confmap/internal/marshaloption.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ type MarshalOption interface {
99

1010
// MarshalOptions is used by (*Conf).Marshal to toggle unmarshaling settings.
1111
// It is in the `internal` package so experimental options can be added in xconfmap.
12-
type MarshalOptions struct{}
12+
type MarshalOptions struct {
13+
// OpaqueUnredacted specifies whether opaque strings should be marshaled unredacted.
14+
OpaqueUnredacted bool
15+
}
1316

1417
type MarshalOptionFunc func(*MarshalOptions)
1518

confmap/xconfmap/confmap.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,11 @@ type ExpandedValue = internal.ExpandedValue
2525
func ToStringMapRaw(conf *confmap.Conf) map[string]any {
2626
return internal.ToStringMapRaw(conf)
2727
}
28+
29+
// WithUnredacted returns a MarshalOption that configures the confmap to
30+
// bypass redaction of opaque string values.
31+
func WithUnredacted() confmap.MarshalOption {
32+
return internal.MarshalOptionFunc(func(opts *internal.MarshalOptions) {
33+
opts.OpaqueUnredacted = true
34+
})
35+
}

confmap/xconfmap/confmap_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package xconfmap
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"go.opentelemetry.io/collector/confmap/internal"
12+
)
13+
14+
func TestWithUnredacted(t *testing.T) {
15+
opt := WithUnredacted()
16+
require.NotNil(t, opt)
17+
18+
opts := &internal.MarshalOptions{}
19+
opt.(internal.MarshalOptionFunc)(opts)
20+
require.True(t, opts.OpaqueUnredacted)
21+
}

otelcol/command_print.go

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -140,36 +140,31 @@ func (pctx *printContext) getPrintableConfig() (any, error) {
140140
return cfg, nil
141141
}
142142

143-
// printUnredactedConfig prints resolved configuration before interpreting
144-
// with the intended types for each component, thus it shows the full
145-
// configuration without considering configuopaque. Use with caution.
143+
// printUnredactedConfig prints the resolved configuration before interpreting
144+
// with the intended types for each component. It uses unredacted mode to
145+
// reveal the full configuration including opaque values. Use with caution.
146146
func (pctx *printContext) printUnredactedConfig() error {
147+
cfg, err := pctx.getPrintableConfig()
148+
if err != nil {
149+
return err
150+
}
151+
147152
if pctx.validate {
148153
// Validation serves prevent revealing invalid data.
149-
cfg, err := pctx.getPrintableConfig()
150-
if err != nil {
151-
return err
152-
}
153154
if err = xconfmap.Validate(cfg); err != nil {
154155
return fmt.Errorf("invalid configuration: %w", err)
155156
}
156-
157157
// Note: we discard the validated configuration.
158158
}
159-
resolver, err := confmap.NewResolver(pctx.set.ConfigProviderSettings.ResolverSettings)
160-
if err != nil {
161-
return fmt.Errorf("failed to create new resolver: %w", err)
162-
}
163-
conf, err := resolver.Resolve(pctx.cmd.Context())
164-
if err != nil {
165-
return fmt.Errorf("error while resolving config: %w", err)
159+
160+
confMap := confmap.New()
161+
if err := confMap.Marshal(cfg, xconfmap.WithUnredacted()); err != nil {
162+
return fmt.Errorf("failed to marshal config: %w", err)
166163
}
167-
return pctx.printConfigData(conf.ToStringMap())
164+
165+
return pctx.printConfigData(confMap.ToStringMap())
168166
}
169167

170-
// printRedactedConfig prints resolved configuration with its assigned
171-
// types, but without validation. Notably, configopaque strings are printed
172-
// as "[redacted]". This is the default.
173168
func (pctx *printContext) printRedactedConfig() error {
174169
cfg, err := pctx.getPrintableConfig()
175170
if err != nil {

otelcol/command_print_test.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestPrintCommand(t *testing.T) {
7575
name: "invalid syntax without validate",
7676
path: invalidConfig1,
7777
errString: "'timeout' time: invalid duration",
78-
errOnlyRedacted: true,
78+
errOnlyRedacted: false,
7979
},
8080
{
8181
name: "validation fail",
@@ -101,23 +101,17 @@ func TestPrintCommand(t *testing.T) {
101101
name: "default field value",
102102
path: defaultConfig,
103103
outString: map[string]string{
104-
"redacted": `timeout: 1s`,
105-
106-
// Since the structure is empty before
107-
// interpretation, no default is expanded.
108-
"unredacted": `e: null`,
104+
"redacted": `timeout: 1s`,
105+
"unredacted": `timeout: 1s`,
109106
},
110107
},
111108
{
112109
name: "field is set json",
113110
ofmt: "json",
114111
path: validConfig,
115112
outString: map[string]string{
116-
// Note: JSON does not format as a time.Duration
117-
"redacted": `"timeout": 5000000000`,
118-
119-
// Note: the original input is "5s"
120-
"unredacted": `"timeout": "5s"`,
113+
"redacted": `"timeout": 5000000000`,
114+
"unredacted": `"timeout": 5000000000`,
121115
},
122116
},
123117
{
@@ -132,12 +126,8 @@ func TestPrintCommand(t *testing.T) {
132126
name: "opaque default",
133127
path: defaultConfig,
134128
outString: map[string]string{
135-
"redacted": `opaque: '[REDACTED]'`,
136-
137-
// Note: the default opaque value does not print,
138-
// the other value is set in defaultConfig so that
139-
// the whole component config is not defaulted.
140-
"unredacted": `other: lala`,
129+
"redacted": `opaque: '[REDACTED]'`,
130+
"unredacted": `opaque: "1234"`,
141131
},
142132
},
143133
}

0 commit comments

Comments
 (0)