Repository: prometheus/prometheus (language: go)

Issue:
config: Need escaping to avoid environment variable expansion in external labels
If I set the feature flag `--enable-feature=expand-external-labels`, I get the nice and sweet expansion of environment variables in configured `external_labels`.

Should I require a literal `$` in one of my label values, I can simply not set the feature flag.

But what if I want the expansion in some label values but require a literal `$` in others?

Also, we would like to promote the feature to stable and get rid of the feature flag eventually (which we can only do with v3 because it is a breaking change). Once we have done so, we definitely need a way to specify a literal `$`.

I propose to use the same escaping as in Bash, i.e. a literal `$` can be created via `\$`.

(I'm actually surprised that https://pkg.go.dev/os#Expand doesn't provide any escaping out of the box.)


Discussion / hints:

Fix patch:
diff --git a/config/config.go b/config/config.go
index 24441d2bcbd..45bafeda50d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -83,6 +83,9 @@ func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, erro
 
 	for i, v := range cfg.GlobalConfig.ExternalLabels {
 		newV := os.Expand(v.Value, func(s string) string {
+			if s == "$" {
+				return "$"
+			}
 			if v := os.Getenv(s); v != "" {
 				return v
 			}
diff --git a/docs/feature_flags.md b/docs/feature_flags.md
index c175e61985c..d203b263a54 100644
--- a/docs/feature_flags.md
+++ b/docs/feature_flags.md
@@ -25,6 +25,7 @@ range vector selectors, and subqueries. More details can be found [here](queryin
 Replace `${var}` or `$var` in the [`external_labels`](configuration/configuration.md#configuration-file)
 values according to the values of the current environment variables. References
 to undefined variables are replaced by the empty string.
+The `$` character can be escaped by using `$$`.
 
 ## Negative offset in PromQL
 


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 695c0188a5d..bdb72dde2a0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1480,12 +1480,16 @@ func TestExpandExternalLabels(t *testing.T) {
 	require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0])
 	require.Equal(t, labels.Label{Name: "baz", Value: "foo${TEST}bar"}, c.GlobalConfig.ExternalLabels[1])
 	require.Equal(t, labels.Label{Name: "foo", Value: "${TEST}"}, c.GlobalConfig.ExternalLabels[2])
+	require.Equal(t, labels.Label{Name: "qux", Value: "foo$${TEST}"}, c.GlobalConfig.ExternalLabels[3])
+	require.Equal(t, labels.Label{Name: "xyz", Value: "foo$$bar"}, c.GlobalConfig.ExternalLabels[4])
 
 	c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger())
 	require.NoError(t, err)
 	require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0])
 	require.Equal(t, labels.Label{Name: "baz", Value: "foobar"}, c.GlobalConfig.ExternalLabels[1])
 	require.Equal(t, labels.Label{Name: "foo", Value: ""}, c.GlobalConfig.ExternalLabels[2])
+	require.Equal(t, labels.Label{Name: "qux", Value: "foo${TEST}"}, c.GlobalConfig.ExternalLabels[3])
+	require.Equal(t, labels.Label{Name: "xyz", Value: "foo$bar"}, c.GlobalConfig.ExternalLabels[4])
 
 	os.Setenv("TEST", "TestValue")
 	c, err = LoadFile("testdata/external_labels.good.yml", false, true, log.NewNopLogger())
@@ -1493,6 +1497,8 @@ func TestExpandExternalLabels(t *testing.T) {
 	require.Equal(t, labels.Label{Name: "bar", Value: "foo"}, c.GlobalConfig.ExternalLabels[0])
 	require.Equal(t, labels.Label{Name: "baz", Value: "fooTestValuebar"}, c.GlobalConfig.ExternalLabels[1])
 	require.Equal(t, labels.Label{Name: "foo", Value: "TestValue"}, c.GlobalConfig.ExternalLabels[2])
+	require.Equal(t, labels.Label{Name: "qux", Value: "foo${TEST}"}, c.GlobalConfig.ExternalLabels[3])
+	require.Equal(t, labels.Label{Name: "xyz", Value: "foo$bar"}, c.GlobalConfig.ExternalLabels[4])
 }
 
 func TestEmptyGlobalBlock(t *testing.T) {
diff --git a/config/testdata/external_labels.good.yml b/config/testdata/external_labels.good.yml
index 828312e4d80..29d28a92bdf 100644
--- a/config/testdata/external_labels.good.yml
+++ b/config/testdata/external_labels.good.yml
@@ -3,3 +3,5 @@ global:
     bar: foo
     foo: ${TEST}
     baz: foo${TEST}bar
+    qux: foo$${TEST}
+    xyz: foo$$bar