diff --git a/config/config.go b/config/config.go index 71a271050..ce029af17 100644 --- a/config/config.go +++ b/config/config.go @@ -40,6 +40,12 @@ import ( "github.com/influxdata/telegraf/plugins/serializers" ) +// envVarPattern is a regex to determine environment variables in the +// config file for substitution. Those should start with a dollar signs. +// Expression modified from +// https://github.com/compose-spec/compose-go/blob/v1.14.0/template/template.go +const envVarPattern = `\\(?P\$)|\$(?i:(?P[_a-z][_a-z0-9]*)|\${(?:(?P[_a-z][_a-z0-9]*(?::?[-+?](.*))?)}|(?P)))` + var ( httpLoadConfigRetryInterval = 10 * time.Second @@ -47,6 +53,9 @@ var ( // be fetched from a remote or read from the filesystem. fetchURLRe = regexp.MustCompile(`^\w+://`) + // envVarRe is the compiled regex of envVarPattern + envVarRe = regexp.MustCompile(envVarPattern) + // Password specified via command-line Password Secret ) @@ -850,12 +859,12 @@ func removeComments(contents []byte) ([]byte, error) { func substituteEnvironment(contents []byte) ([]byte, error) { envMap := utils.GetAsEqualsMap(os.Environ()) - retVal, err := template.Substitute(string(contents), func(k string) (string, bool) { + retVal, err := template.SubstituteWith(string(contents), func(k string) (string, bool) { if v, ok := envMap[k]; ok { return v, ok } return "", false - }) + }, envVarRe) var invalidTmplError *template.InvalidTemplateError if err != nil && !errors.As(err, &invalidTmplError) { return nil, err diff --git a/config/config_test.go b/config/config_test.go index 0d989db20..d0b6e6cbe 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -71,7 +71,7 @@ func TestConfig_LoadSingleInputWithEnvVars(t *testing.T) { input := inputs.Inputs["memcached"]().(*MockupInputPlugin) input.Servers = []string{"192.168.1.1"} - input.Command = `Raw command which may or may not contain # in it + input.Command = `Raw command which may or may not contain # or ${var} in it # is unique` filter := models.Filter{ diff --git a/config/internal_test.go b/config/internal_test.go index 2d04926f1..ecf13e7bd 100644 --- a/config/internal_test.go +++ b/config/internal_test.go @@ -25,17 +25,17 @@ func TestEnvironmentSubstitution(t *testing.T) { t.Setenv("TEST_ENV1", "VALUE1") t.Setenv("TEST_ENV2", "VALUE2") }, - contents: "A string with ${TEST_ENV1}, $TEST_ENV2 and $TEST_ENV1 as repeated", + contents: "A string with $${TEST_ENV1}, $TEST_ENV2 and $TEST_ENV1 as repeated", expected: "A string with VALUE1, VALUE2 and VALUE1 as repeated", }, { name: "Env not set", - contents: "Env variable ${NOT_SET} will be empty", + contents: "Env variable $${NOT_SET} will be empty", expected: "Env variable will be empty", // Two spaces present }, { name: "Env not set, fallback to default", - contents: "Env variable ${THIS_IS_ABSENT:-Fallback}", + contents: "Env variable $${THIS_IS_ABSENT:-Fallback}", expected: "Env variable Fallback", }, { @@ -43,7 +43,7 @@ func TestEnvironmentSubstitution(t *testing.T) { setEnv: func(t *testing.T) { t.Setenv("MY_ENV1", "VALUE1") }, - contents: "Env variable ${MY_ENV1:-Fallback}", + contents: "Env variable $${MY_ENV1:-Fallback}", expected: "Env variable VALUE1", }, { @@ -52,17 +52,17 @@ func TestEnvironmentSubstitution(t *testing.T) { t.Setenv("MY_VAR", "VALUE") t.Setenv("MY_VAR2", "VALUE2") }, - contents: "Env var ${MY_VAR} is set, with $MY_VAR syntax and default on this ${MY_VAR1:-Substituted}, no default on this ${MY_VAR2:-NoDefault}", + contents: "Env var $${MY_VAR} is set, with $MY_VAR syntax and default on this $${MY_VAR1:-Substituted}, no default on this $${MY_VAR2:-NoDefault}", expected: "Env var VALUE is set, with VALUE syntax and default on this Substituted, no default on this VALUE2", }, { name: "Default has special chars", - contents: `Not recommended but supported ${MY_VAR:-Default with special chars Supported#$\"}`, + contents: `Not recommended but supported $${MY_VAR:-Default with special chars Supported#$\"}`, expected: `Not recommended but supported Default with special chars Supported#$\"`, // values are escaped }, { name: "unset error", - contents: "Contains ${THIS_IS_NOT_SET?unset-error}", + contents: "Contains $${THIS_IS_NOT_SET?unset-error}", wantErr: true, errSubstring: "unset-error", }, @@ -71,7 +71,7 @@ func TestEnvironmentSubstitution(t *testing.T) { setEnv: func(t *testing.T) { t.Setenv("ENV_EMPTY", "") }, - contents: "Contains ${ENV_EMPTY:?empty-error}", + contents: "Contains $${ENV_EMPTY:?empty-error}", wantErr: true, errSubstring: "empty-error", }, @@ -80,9 +80,33 @@ func TestEnvironmentSubstitution(t *testing.T) { setEnv: func(t *testing.T) { t.Setenv("FALLBACK", "my-fallback") }, - contents: "Should output ${NOT_SET:-${FALLBACK}}", + contents: "Should output $${NOT_SET:-${FALLBACK}}", expected: "Should output my-fallback", }, + { + name: "leave alone single dollar expressions #13432", + setEnv: func(t *testing.T) { + t.Setenv("MYVAR", "my-variable") + }, + contents: "Should output ${MYVAR}", + expected: "Should output ${MYVAR}", + }, + { + name: "leave alone escaped expressions (backslash)", + setEnv: func(t *testing.T) { + t.Setenv("MYVAR", "my-variable") + }, + contents: `Should output \$MYVAR`, + expected: "Should output $MYVAR", + }, + { + name: "double dollar no brackets", + setEnv: func(t *testing.T) { + t.Setenv("MYVAR", "my-variable") + }, + contents: `Should output $$MYVAR`, + expected: "Should output $my-variable", + }, } for _, tt := range tests { diff --git a/config/testdata/single_plugin_env_vars.toml b/config/testdata/single_plugin_env_vars.toml index 903bdab3e..773e09766 100644 --- a/config/testdata/single_plugin_env_vars.toml +++ b/config/testdata/single_plugin_env_vars.toml @@ -10,27 +10,26 @@ # file would generate. # # Environment variables can be used anywhere in this config file, simply surround -# them with ${}. For strings the variable must be within quotes (ie, "${STR_VAR}"), -# for numbers and booleans they should be plain (ie, ${INT_VAR}, ${BOOL_VAR}) +# them with $${}. For strings the variable must be within quotes (ie, "$${STR_VAR}"), +# for numbers and booleans they should be plain (ie, $${INT_VAR}, $${BOOL_VAR}) [[inputs.memcached]] # this comment line will be ignored by the parser - servers = ["$MY_TEST_SERVER"] - namepass = ["metricname1", "ip_${MY_TEST_SERVER}_name"] # this comment will be ignored as well + servers = ["$MY_TEST_SERVER"] + namepass = ["metricname1", "ip_$${MY_TEST_SERVER}_name"] # this comment will be ignored as well namedrop = ["metricname2"] fieldpass = ["some", "strings"] fielddrop = ["other", "stuff"] interval = "$TEST_INTERVAL" ##### this input is provided to test multiline strings command = """ -Raw command which may or may not contain # in it +Raw command which may or may not contain # or ${var} in it # is unique""" # Multiline comment black starting with # [inputs.memcached.tagpass] - goodtag = ["mytag", """tagwith#value""", + goodtag = ["mytag", """tagwith#value""", # comment in between array items # should ignore "quotes" in comments '''TagWithMultilineSyntax''', ## ignore this comment ] # hastag [inputs.memcached.tagdrop] badtag = ["othertag"] - \ No newline at end of file