Skip to content

Commit 5060166

Browse files
authored
[configoptional] Add support for setting an 'enabled' field under a feature gate (#13995)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description <!-- Issue number if applicable --> Adds support for disabling or enabling optional fields through an `enabled` key under an alpha feature gate, `configoptional.AddEnabledField`. For example, the following configuration becomes valid: ```yaml receivers: otlp: protocols: grpc: enabled: true exporters: nop: service: pipelines: traces: receivers: [otlp] exporters: [nop] ``` and is equivalent to: ```yaml receivers: otlp: protocols: grpc: exporters: nop: service: pipelines: traces: receivers: [otlp] exporters: [nop] ``` #### Link to tracking issue Fixes #13894 Updates #14021
1 parent c480657 commit 5060166

File tree

4 files changed

+229
-2
lines changed

4 files changed

+229
-2
lines changed
Lines changed: 25 additions & 0 deletions
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: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: pkg/config/configoptional
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Adds new `configoptional.AddEnabledField` feature gate that allows users to explicitly disable a `configoptional.Optional` through a new `enabled` field.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [14021]
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:
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: []

config/configoptional/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/stretchr/testify v1.11.1
77
go.opentelemetry.io/collector/confmap v1.44.0
88
go.opentelemetry.io/collector/confmap/xconfmap v0.138.0
9+
go.opentelemetry.io/collector/featuregate v1.44.0
910
go.uber.org/goleak v1.3.0
1011
)
1112

@@ -20,7 +21,6 @@ require (
2021
github.com/mitchellh/copystructure v1.2.0 // indirect
2122
github.com/mitchellh/reflectwalk v1.0.2 // indirect
2223
github.com/pmezard/go-difflib v1.0.0 // indirect
23-
go.opentelemetry.io/collector/featuregate v1.44.0 // indirect
2424
go.uber.org/multierr v1.11.0 // indirect
2525
go.uber.org/zap v1.27.0 // indirect
2626
go.yaml.in/yaml/v3 v3.0.4 // indirect

config/configoptional/optional.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"go.opentelemetry.io/collector/confmap"
1313
"go.opentelemetry.io/collector/confmap/xconfmap"
14+
"go.opentelemetry.io/collector/featuregate"
1415
)
1516

1617
type flavor int
@@ -26,6 +27,7 @@ const (
2627
// It supports a third flavor for struct types: Default(defaultVal).
2728
//
2829
// For struct types, it supports unmarshaling from a configuration source.
30+
// For struct types, it supports an 'enabled' field to explicitly disable a section.
2931
// The zero value of Optional is None.
3032
type Optional[T any] struct {
3133
// value is the value of the Optional.
@@ -165,6 +167,17 @@ func (o *Optional[T]) GetOrInsertDefault() *T {
165167

166168
var _ confmap.Unmarshaler = (*Optional[any])(nil)
167169

170+
var (
171+
addEnabledFieldFeatureGateID = "configoptional.AddEnabledField"
172+
addEnabledFieldFeatureGate = featuregate.GlobalRegistry().MustRegister(
173+
addEnabledFieldFeatureGateID,
174+
featuregate.StageAlpha,
175+
featuregate.WithRegisterFromVersion("v0.138.0"),
176+
featuregate.WithRegisterDescription("Allows optional fields to be toggled via an 'enabled' field."),
177+
featuregate.WithRegisterReferenceURL("https://github.com/open-telemetry/opentelemetry-collector/issues/14021"),
178+
)
179+
)
180+
168181
// Unmarshal the configuration into the Optional value.
169182
//
170183
// The behavior of this method depends on the state of the Optional:
@@ -173,6 +186,11 @@ var _ confmap.Unmarshaler = (*Optional[any])(nil)
173186
// - Default[T](val), equivalent to unmarshaling into a field of type T with base value val,
174187
// using val without overrides from the configuration if the configuration is nil.
175188
//
189+
// (Under the `configoptional.AddEnabledField` feature gate)
190+
// If the configuration contains an 'enabled' field:
191+
// - if enabled is true: the Optional becomes Some after unmarshaling.
192+
// - if enabled is false: the Optional becomes None regardless of other configuration values.
193+
//
176194
// T must be derefenceable to a type with struct kind and not have an 'enabled' field.
177195
// Scalar values are not supported.
178196
func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
@@ -186,11 +204,26 @@ func (o *Optional[T]) Unmarshal(conf *confmap.Conf) error {
186204
return nil
187205
}
188206

207+
isEnabled := true
208+
if addEnabledFieldFeatureGate.IsEnabled() && conf.IsSet("enabled") {
209+
enabled := conf.Get("enabled")
210+
conf.Delete("enabled")
211+
var ok bool
212+
if isEnabled, ok = enabled.(bool); !ok {
213+
return fmt.Errorf("unexpected type %T for 'enabled': got '%v' value expected 'true' or 'false'", enabled, enabled)
214+
}
215+
}
216+
189217
if err := conf.Unmarshal(&o.value); err != nil {
190218
return err
191219
}
192220

193-
o.flavor = someFlavor
221+
if isEnabled {
222+
o.flavor = someFlavor
223+
} else {
224+
o.flavor = noneFlavor
225+
}
226+
194227
return nil
195228
}
196229

config/configoptional/optional_test.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.opentelemetry.io/collector/confmap"
1515
"go.opentelemetry.io/collector/confmap/confmaptest"
1616
"go.opentelemetry.io/collector/confmap/xconfmap"
17+
"go.opentelemetry.io/collector/featuregate"
1718
)
1819

1920
type Config[T any] struct {
@@ -377,6 +378,174 @@ func TestUnmarshalOptional(t *testing.T) {
377378
}
378379
}
379380

381+
func TestAddFieldEnabledFeatureGate(t *testing.T) {
382+
tests := []struct {
383+
name string
384+
config map[string]any
385+
defaultCfg Config[Sub]
386+
expectedSub bool
387+
expectedFoo string
388+
}{
389+
{
390+
name: "none_with_enabled_true",
391+
config: map[string]any{
392+
"sub": map[string]any{
393+
"enabled": true,
394+
"foo": "bar",
395+
},
396+
},
397+
defaultCfg: Config[Sub]{
398+
Sub1: None[Sub](),
399+
},
400+
expectedSub: true,
401+
expectedFoo: "bar",
402+
},
403+
{
404+
name: "none_with_enabled_false",
405+
config: map[string]any{
406+
"sub": map[string]any{
407+
"enabled": false,
408+
"foo": "bar",
409+
},
410+
},
411+
defaultCfg: Config[Sub]{
412+
Sub1: None[Sub](),
413+
},
414+
expectedSub: false,
415+
},
416+
{
417+
name: "none_with_enabled_false_no_other_config",
418+
config: map[string]any{
419+
"sub": map[string]any{
420+
"enabled": false,
421+
},
422+
},
423+
defaultCfg: Config[Sub]{
424+
Sub1: None[Sub](),
425+
},
426+
expectedSub: false,
427+
},
428+
{
429+
name: "default_with_enabled_true",
430+
config: map[string]any{
431+
"sub": map[string]any{
432+
"enabled": true,
433+
"foo": "bar",
434+
},
435+
},
436+
defaultCfg: Config[Sub]{
437+
Sub1: Default(subDefault),
438+
},
439+
expectedSub: true,
440+
expectedFoo: "bar",
441+
},
442+
{
443+
name: "default_with_enabled_false",
444+
config: map[string]any{
445+
"sub": map[string]any{
446+
"enabled": false,
447+
"foo": "bar",
448+
},
449+
},
450+
defaultCfg: Config[Sub]{
451+
Sub1: Default(subDefault),
452+
},
453+
expectedSub: false,
454+
},
455+
{
456+
name: "default_with_enabled_false_no_other_config",
457+
config: map[string]any{
458+
"sub": map[string]any{
459+
"enabled": false,
460+
},
461+
},
462+
defaultCfg: Config[Sub]{
463+
Sub1: Default(subDefault),
464+
},
465+
expectedSub: false,
466+
},
467+
{
468+
name: "some_with_enabled_true",
469+
config: map[string]any{
470+
"sub": map[string]any{
471+
"enabled": true,
472+
"foo": "baz",
473+
},
474+
},
475+
defaultCfg: Config[Sub]{
476+
Sub1: Some(Sub{
477+
Foo: "foobar",
478+
}),
479+
},
480+
expectedSub: true,
481+
expectedFoo: "baz",
482+
},
483+
{
484+
name: "some_with_enabled_false",
485+
config: map[string]any{
486+
"sub": map[string]any{
487+
"enabled": false,
488+
"foo": "baz",
489+
},
490+
},
491+
defaultCfg: Config[Sub]{
492+
Sub1: Some(Sub{
493+
Foo: "foobar",
494+
}),
495+
},
496+
expectedSub: false,
497+
},
498+
{
499+
name: "some_with_enabled_false_no_other_config",
500+
config: map[string]any{
501+
"sub": map[string]any{
502+
"enabled": false,
503+
},
504+
},
505+
defaultCfg: Config[Sub]{
506+
Sub1: Some(Sub{
507+
Foo: "foobar",
508+
}),
509+
},
510+
expectedSub: false,
511+
},
512+
}
513+
514+
oldVal := addEnabledFieldFeatureGate.IsEnabled()
515+
require.NoError(t, featuregate.GlobalRegistry().Set(addEnabledFieldFeatureGateID, true))
516+
defer func() { require.NoError(t, featuregate.GlobalRegistry().Set(addEnabledFieldFeatureGateID, oldVal)) }()
517+
518+
for _, test := range tests {
519+
t.Run(test.name, func(t *testing.T) {
520+
cfg := test.defaultCfg
521+
conf := confmap.NewFromStringMap(test.config)
522+
require.NoError(t, conf.Unmarshal(&cfg))
523+
require.Equal(t, test.expectedSub, cfg.Sub1.HasValue())
524+
if test.expectedSub {
525+
require.Equal(t, test.expectedFoo, cfg.Sub1.Get().Foo)
526+
}
527+
})
528+
}
529+
}
530+
531+
func TestUnmarshalErrorEnabledInvalidType(t *testing.T) {
532+
oldVal := addEnabledFieldFeatureGate.IsEnabled()
533+
require.NoError(t, featuregate.GlobalRegistry().Set(addEnabledFieldFeatureGateID, true))
534+
defer func() { require.NoError(t, featuregate.GlobalRegistry().Set(addEnabledFieldFeatureGateID, oldVal)) }()
535+
536+
cm := confmap.NewFromStringMap(map[string]any{
537+
"sub": map[string]any{
538+
"enabled": "something",
539+
"foo": "bar",
540+
},
541+
})
542+
cfg := Config[Sub]{
543+
Sub1: None[Sub](),
544+
}
545+
err := cm.Unmarshal(&cfg)
546+
require.ErrorContains(t, err, "unexpected type string for 'enabled': got 'something' value expected 'true' or 'false'")
547+
}
548+
380549
func TestUnmarshalErrorEnabledField(t *testing.T) {
381550
cm := confmap.NewFromStringMap(map[string]any{
382551
"enabled": true,

0 commit comments

Comments
 (0)