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] Default config env variable expansion #2231

Merged
merged 15 commits into from
Dec 3, 2020
43 changes: 43 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ func loadExtensions(exts map[string]interface{}, factories map[configmodels.Type
// Create the default config for this extension
extensionCfg := factory.CreateDefaultConfig()
extensionCfg.SetName(fullName)
expandEnvLoadedConfig(extensionCfg)

// Now that the default config struct is created we can Unmarshal into it
// and it will apply user-defined config on top of the default.
Expand Down Expand Up @@ -305,6 +306,7 @@ func LoadReceiver(componentConfig *viper.Viper, typeStr configmodels.Type, fullN
// Create the default config for this receiver.
receiverCfg := factory.CreateDefaultConfig()
receiverCfg.SetName(fullName)
expandEnvLoadedConfig(receiverCfg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be done in loadReceivers instead of here? It looks odd to me that this is different that for the rest of component kinds

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because receivers are handled differently since #658, it's by design. Consider this solved.


// Now that the default config struct is created we can Unmarshal into it
// and it will apply user-defined config on top of the default.
Expand Down Expand Up @@ -377,6 +379,7 @@ func loadExporters(exps map[string]interface{}, factories map[configmodels.Type]
// Create the default config for this exporter
exporterCfg := factory.CreateDefaultConfig()
exporterCfg.SetName(fullName)
expandEnvLoadedConfig(exporterCfg)

// Now that the default config struct is created we can Unmarshal into it
// and it will apply user-defined config on top of the default.
Expand Down Expand Up @@ -419,6 +422,7 @@ func loadProcessors(procs map[string]interface{}, factories map[configmodels.Typ
// Create the default config for this processor.
processorCfg := factory.CreateDefaultConfig()
processorCfg.SetName(fullName)
expandEnvLoadedConfig(processorCfg)

// Now that the default config struct is created we can Unmarshal into it
// and it will apply user-defined config on top of the default.
Expand Down Expand Up @@ -671,6 +675,45 @@ func expandStringValues(value interface{}) interface{} {
}
}

// expandEnvLoadedConfig is a utility function that goes recursively through a config object
// and tries to expand environment variables in its string fields.
func expandEnvLoadedConfig(s interface{}) {
expandEnvLoadedConfigPointer(s)
}

func expandEnvLoadedConfigPointer(s interface{}) {
// Check that the value given is indeed a pointer, otherwise safely stop the search here
value := reflect.ValueOf(s)
if value.Kind() != reflect.Ptr {
return
}
// Run expandLoadedConfigValue on the value behind the pointer
expandEnvLoadedConfigValue(value.Elem())
}

func expandEnvLoadedConfigValue(value reflect.Value) {
// The value given is a string, we expand it (if allowed)
if value.Kind() == reflect.String && value.CanSet() {
value.SetString(expandEnv(value.String()))
}
// The value given is a struct, we go through its fields
if value.Kind() == reflect.Struct {
for i := 0; i < value.NumField(); i++ {
field := value.Field(i) // Returns the content of the field
if field.CanSet() { // Only try to modify a field if it can be modified (eg. skip unexported private fields)
switch field.Kind() {
case reflect.String: // The current field is a string, we want to expand it
field.SetString(expandEnv(field.String())) // Expand env variables in the string
case reflect.Ptr: // The current field is a pointer
expandEnvLoadedConfigPointer(field.Interface()) // Run the expansion function on the pointer
case reflect.Struct: // The current field is a nested struct
expandEnvLoadedConfigValue(field) // Go through the nested struct
}
}
}
}
}

func expandEnv(s string) string {
return os.Expand(s, func(str string) string {
// This allows escaping environment variable substitution via $$, e.g.
Expand Down
224 changes: 224 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,227 @@ func loadConfigFile(t *testing.T, fileName string, factories component.Factories
}
return cfg, ValidateConfig(cfg, zap.NewNop())
}

type nestedConfig struct {
NestedStringValue string
NestedIntValue int
}

type testConfig struct {
configmodels.ExporterSettings

NestedConfigPtr *nestedConfig
NestedConfigValue nestedConfig
StringValue string
StringPtrValue *string
IntValue int
}

func TestExpandEnvLoadedConfig(t *testing.T) {
assert.NoError(t, os.Setenv("NESTED_VALUE", "replaced_nested_value"))
assert.NoError(t, os.Setenv("VALUE", "replaced_value"))
assert.NoError(t, os.Setenv("PTR_VALUE", "replaced_ptr_value"))

defer func() {
assert.NoError(t, os.Unsetenv("NESTED_VALUE"))
assert.NoError(t, os.Unsetenv("VALUE"))
assert.NoError(t, os.Unsetenv("PTR_VALUE"))
}()

testString := "$PTR_VALUE"

config := &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 2,
},
StringValue: "$VALUE",
StringPtrValue: &testString,
IntValue: 3,
}

expandEnvLoadedConfig(config)

replacedTestString := "replaced_ptr_value"

assert.Equal(t, &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 2,
},
StringValue: "replaced_value",
StringPtrValue: &replacedTestString,
IntValue: 3,
}, config)
}

func TestExpandEnvLoadedConfigEscapedEnv(t *testing.T) {
assert.NoError(t, os.Setenv("NESTED_VALUE", "replaced_nested_value"))
assert.NoError(t, os.Setenv("ESCAPED_VALUE", "replaced_escaped_value"))
assert.NoError(t, os.Setenv("ESCAPED_PTR_VALUE", "replaced_escaped_pointer_value"))

defer func() {
assert.NoError(t, os.Unsetenv("NESTED_VALUE"))
assert.NoError(t, os.Unsetenv("ESCAPED_VALUE"))
assert.NoError(t, os.Unsetenv("ESCAPED_PTR_VALUE"))
}()

testString := "$$ESCAPED_PTR_VALUE"

config := &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 2,
},
StringValue: "$$ESCAPED_VALUE",
StringPtrValue: &testString,
IntValue: 3,
}

expandEnvLoadedConfig(config)

replacedTestString := "$ESCAPED_PTR_VALUE"

assert.Equal(t, &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 2,
},
StringValue: "$ESCAPED_VALUE",
StringPtrValue: &replacedTestString,
IntValue: 3,
}, config)
}

func TestExpandEnvLoadedConfigMissingEnv(t *testing.T) {
assert.NoError(t, os.Setenv("NESTED_VALUE", "replaced_nested_value"))

defer func() {
assert.NoError(t, os.Unsetenv("NESTED_VALUE"))
}()

testString := "$PTR_VALUE"

config := &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "$NESTED_VALUE",
NestedIntValue: 2,
},
StringValue: "$VALUE",
StringPtrValue: &testString,
IntValue: 3,
}

expandEnvLoadedConfig(config)

replacedTestString := ""

assert.Equal(t, &testConfig{
ExporterSettings: configmodels.ExporterSettings{
TypeVal: configmodels.Type("test"),
NameVal: "test",
},
NestedConfigPtr: &nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 1,
},
NestedConfigValue: nestedConfig{
NestedStringValue: "replaced_nested_value",
NestedIntValue: 2,
},
StringValue: "",
StringPtrValue: &replacedTestString,
IntValue: 3,
}, config)
}

func TestExpandEnvLoadedConfigNil(t *testing.T) {
var config *testConfig

// This should safely do nothing
expandEnvLoadedConfig(config)

assert.Equal(t, (*testConfig)(nil), config)
}

func TestExpandEnvLoadedConfigNoPointer(t *testing.T) {
assert.NoError(t, os.Setenv("VALUE", "replaced_value"))

config := testConfig{
StringValue: "$VALUE",
}

// This should do nothing as config is not a pointer
expandEnvLoadedConfig(config)

assert.Equal(t, testConfig{
StringValue: "$VALUE",
}, config)
}

type testUnexportedConfig struct {
configmodels.ExporterSettings

unexportedStringValue string
ExportedStringValue string
}

func TestExpandEnvLoadedConfigUnexportedField(t *testing.T) {
assert.NoError(t, os.Setenv("VALUE", "replaced_value"))

defer func() {
assert.NoError(t, os.Unsetenv("VALUE"))
}()

config := &testUnexportedConfig{
unexportedStringValue: "$VALUE",
ExportedStringValue: "$VALUE",
}

expandEnvLoadedConfig(config)

assert.Equal(t, &testUnexportedConfig{
unexportedStringValue: "$VALUE",
ExportedStringValue: "replaced_value",
}, config)
}