Skip to content

Commit 3510de1

Browse files
authored
Add ability to override configuration settings using environment variables (#2147)
* Add ability to override configuration settings using environment variables Signed-off-by: Wing924 <[email protected]>
1 parent 7b0bb68 commit 3510de1

File tree

5 files changed

+178
-21
lines changed

5 files changed

+178
-21
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* `--experimental.distributor.user-subring-size`
1818
* [FEATURE] Added flag `-experimental.ruler.enable-api` to enable the ruler api which implements the Prometheus API `/api/v1/rules` and `/api/v1/alerts` endpoints under the configured `-http.prefix`. #1999
1919
* [FEATURE] Added sharding support to compactor when using the experimental TSDB blocks storage. #2113
20+
* [FEATURE] Add ability to override YAML config file settings using environment variables. #2147
21+
* `-config.expand-env`
2022
* [ENHANCEMENT] Add `status` label to `cortex_alertmanager_configs` metric to gauge the number of valid and invalid configs. #2125
2123
* [ENHANCEMENT] Cassandra Authentication: added the `custom_authenticators` config option that allows users to authenticate with cassandra clusters using password authenticators that are not approved by default in [gocql](https://github.com/gocql/gocql/blob/81b8263d9fe526782a588ef94d3fa5c6148e5d67/conn.go#L27) #2093
2224
* [ENHANCEMENT] Experimental TSDB: Export TSDB Syncer metrics from Compactor component, they are prefixed with `cortex_compactor_`. #2023

cmd/cortex/main.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math/rand"
88
"os"
99
"runtime"
10+
"strings"
1011
"time"
1112

1213
"github.com/go-kit/kit/log/level"
@@ -25,7 +26,10 @@ func init() {
2526
prometheus.MustRegister(version.NewCollector("cortex"))
2627
}
2728

28-
const configFileOption = "config.file"
29+
const (
30+
configFileOption = "config.file"
31+
configExpandENV = "config.expand-env"
32+
)
2933

3034
var testMode = false
3135

@@ -37,14 +41,14 @@ func main() {
3741
mutexProfileFraction int
3842
)
3943

40-
configFile := parseConfigFileParameter()
44+
configFile, expandENV := parseConfigFileParameter(os.Args[1:])
4145

4246
// This sets default values from flags to the config.
4347
// It needs to be called before parsing the config file!
4448
flagext.RegisterFlags(&cfg)
4549

4650
if configFile != "" {
47-
if err := LoadConfig(configFile, &cfg); err != nil {
51+
if err := LoadConfig(configFile, expandENV, &cfg); err != nil {
4852
fmt.Fprintf(os.Stderr, "error loading config from %s: %v\n", configFile, err)
4953
if testMode {
5054
return
@@ -53,8 +57,10 @@ func main() {
5357
}
5458
}
5559

56-
// Ignore -config.file here, since it was already parsed, but it's still present on command line.
60+
// Ignore -config.file and -config.expand-env here, since it was already parsed, but it's still present on command line.
5761
flagext.IgnoredFlag(flag.CommandLine, configFileOption, "Configuration file to load.")
62+
flagext.IgnoredFlag(flag.CommandLine, configExpandENV, "Expands ${var} or $var in config according to the values of the environment variables.")
63+
5864
flag.IntVar(&eventSampleRate, "event.sample-rate", 0, "How often to sample observability events (0 = never).")
5965
flag.IntVar(&ballastBytes, "mem-ballast-size-bytes", 0, "Size of memory ballast to allocate.")
6066
flag.IntVar(&mutexProfileFraction, "debug.mutex-profile-fraction", 0, "Fraction at which mutex profile vents will be reported, 0 to disable")
@@ -108,37 +114,38 @@ func main() {
108114
util.CheckFatal("initializing cortex", err)
109115
}
110116

111-
// Parse -config.file option via separate flag set, to avoid polluting default one and calling flag.Parse on it twice.
112-
func parseConfigFileParameter() string {
113-
var configFile = ""
117+
// Parse -config.file and -config.expand-env option via separate flag set, to avoid polluting default one and calling flag.Parse on it twice.
118+
func parseConfigFileParameter(args []string) (configFile string, expandEnv bool) {
114119
// ignore errors and any output here. Any flag errors will be reported by main flag.Parse() call.
115-
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
120+
fs := flag.NewFlagSet("", flag.ContinueOnError)
116121
fs.SetOutput(ioutil.Discard)
117-
fs.StringVar(&configFile, configFileOption, "", "") // usage not used in this function.
118122

119-
// Try to find -config.file option in the flags. As Parsing stops on the first error, eg. unknown flag, we simply
123+
// usage not used in these functions.
124+
fs.StringVar(&configFile, configFileOption, "", "")
125+
fs.BoolVar(&expandEnv, configExpandENV, false, "")
126+
127+
// Try to find -config.file and -config.expand-env option in the flags. As Parsing stops on the first error, eg. unknown flag, we simply
120128
// try remaining parameters until we find config flag, or there are no params left.
121129
// (ContinueOnError just means that flag.Parse doesn't call panic or os.Exit, but it returns error, which we ignore)
122-
args := os.Args[1:]
123130
for len(args) > 0 {
124131
_ = fs.Parse(args)
125-
if configFile != "" {
126-
// found (!)
127-
break
128-
}
129132
args = args[1:]
130133
}
131134

132-
return configFile
135+
return
133136
}
134137

135138
// LoadConfig read YAML-formatted config from filename into cfg.
136-
func LoadConfig(filename string, cfg *cortex.Config) error {
139+
func LoadConfig(filename string, expandENV bool, cfg *cortex.Config) error {
137140
buf, err := ioutil.ReadFile(filename)
138141
if err != nil {
139142
return errors.Wrap(err, "Error reading config file")
140143
}
141144

145+
if expandENV {
146+
buf = expandEnv(buf)
147+
}
148+
142149
err = yaml.UnmarshalStrict(buf, cfg)
143150
if err != nil {
144151
return errors.Wrap(err, "Error parsing config file")
@@ -155,3 +162,19 @@ func DumpYaml(cfg *cortex.Config) {
155162
fmt.Printf("%s\n", out)
156163
}
157164
}
165+
166+
// expandEnv replaces ${var} or $var in config according to the values of the current environment variables.
167+
// The replacement is case-sensitive. References to undefined variables are replaced by the empty string.
168+
// A default value can be given by using the form ${var:default value}.
169+
func expandEnv(config []byte) []byte {
170+
return []byte(os.Expand(string(config), func(key string) string {
171+
keyAndDefault := strings.SplitN(key, ":", 2)
172+
key = keyAndDefault[0]
173+
174+
v := os.Getenv(key)
175+
if v == "" && len(keyAndDefault) == 2 {
176+
v = keyAndDefault[1] // Set value to the default.
177+
}
178+
return v
179+
}))
180+
}

cmd/cortex/main_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import (
66
"io"
77
"io/ioutil"
88
"os"
9+
"strings"
910
"sync"
1011
"testing"
1112

13+
"github.com/stretchr/testify/assert"
1214
"github.com/stretchr/testify/require"
1315
)
1416

@@ -61,6 +63,17 @@ func TestFlagParsing(t *testing.T) {
6163
stdoutMessage: "target: ingester\n",
6264
},
6365

66+
"config without expand-env": {
67+
yaml: "target: $TARGET",
68+
stderrMessage: "Error parsing config file: unrecognised module name: $TARGET\n",
69+
},
70+
71+
"config with expand-env": {
72+
arguments: []string{"-config.expand-env"},
73+
yaml: "target: $TARGET",
74+
stdoutMessage: "target: ingester\n",
75+
},
76+
6477
"config with arguments override": {
6578
yaml: "target: ingester",
6679
arguments: []string{"-target=distributor"},
@@ -70,12 +83,14 @@ func TestFlagParsing(t *testing.T) {
7083
// we cannot test the happy path, as cortex would then fully start
7184
} {
7285
t.Run(name, func(t *testing.T) {
86+
_ = os.Setenv("TARGET", "ingester")
7387
testSingle(t, tc.arguments, tc.yaml, []byte(tc.stdoutMessage), []byte(tc.stderrMessage))
7488
})
7589
}
7690
}
7791

7892
func testSingle(t *testing.T, arguments []string, yaml string, stdoutMessage, stderrMessage []byte) {
93+
t.Helper()
7994
oldArgs, oldStdout, oldStderr, oldTestMode := os.Args, os.Stdout, os.Stderr, testMode
8095
defer func() {
8196
os.Stdout = oldStdout
@@ -167,3 +182,74 @@ func (co *capturedOutput) Done() (stdout []byte, stderr []byte) {
167182

168183
return co.stdoutBuf.Bytes(), co.stderrBuf.Bytes()
169184
}
185+
186+
func TestExpandEnv(t *testing.T) {
187+
var tests = []struct {
188+
in string
189+
out string
190+
}{
191+
// Environment variables can be specified as ${env} or $env.
192+
{"x$y", "xy"},
193+
{"x${y}", "xy"},
194+
195+
// Environment variables are case-sensitive. Neither are replaced.
196+
{"x$Y", "x"},
197+
{"x${Y}", "x"},
198+
199+
// Defaults can only be specified when using braces.
200+
{"x${Z:D}", "xD"},
201+
{"x${Z:A B C D}", "xA B C D"}, // Spaces are allowed in the default.
202+
{"x${Z:}", "x"},
203+
204+
// Defaults don't work unless braces are used.
205+
{"x$y:D", "xy:D"},
206+
}
207+
208+
for _, test := range tests {
209+
test := test
210+
t.Run(test.in, func(t *testing.T) {
211+
_ = os.Setenv("y", "y")
212+
output := expandEnv([]byte(test.in))
213+
assert.Equal(t, test.out, string(output), "Input: %s", test.in)
214+
})
215+
}
216+
}
217+
218+
func TestParseConfigFileParameter(t *testing.T) {
219+
var tests = []struct {
220+
args string
221+
configFile string
222+
expandENV bool
223+
}{
224+
{"", "", false},
225+
{"--foo", "", false},
226+
{"-f -a", "", false},
227+
228+
{"--config.file=foo", "foo", false},
229+
{"--config.file foo", "foo", false},
230+
{"--config.file=foo --config.expand-env", "foo", true},
231+
{"--config.expand-env --config.file=foo", "foo", true},
232+
233+
{"--opt1 --config.file=foo", "foo", false},
234+
{"--opt1 --config.file foo", "foo", false},
235+
{"--opt1 --config.file=foo --config.expand-env", "foo", true},
236+
{"--opt1 --config.expand-env --config.file=foo", "foo", true},
237+
238+
{"--config.file=foo --opt1", "foo", false},
239+
{"--config.file foo --opt1", "foo", false},
240+
{"--config.file=foo --config.expand-env --opt1", "foo", true},
241+
{"--config.expand-env --config.file=foo --opt1", "foo", true},
242+
243+
{"--config.file=foo --opt1 --config.expand-env", "foo", true},
244+
{"--config.expand-env --opt1 --config.file=foo", "foo", true},
245+
}
246+
for _, test := range tests {
247+
test := test
248+
t.Run(test.args, func(t *testing.T) {
249+
args := strings.Split(test.args, " ")
250+
configFile, expandENV := parseConfigFileParameter(args)
251+
assert.Equal(t, test.configFile, configFile)
252+
assert.Equal(t, test.expandENV, expandENV)
253+
})
254+
}
255+
}

docs/configuration/config-file-reference.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Cortex can be configured using a YAML file - specified using the `-config.file`
1111

1212
To specify which configuration file to load, pass the `-config.file` flag at the command line. The file is written in [YAML format](https://en.wikipedia.org/wiki/YAML), defined by the scheme below. Brackets indicate that a parameter is optional.
1313

14-
Generic placeholders are defined as follows:
14+
### Generic placeholders
1515

1616
* `<boolean>`: a boolean that can take the values `true` or `false`
1717
* `<int>`: any integer matching the regular expression `[1-9]+[0-9]*`
@@ -20,7 +20,30 @@ Generic placeholders are defined as follows:
2020
* `<url>`: an URL
2121
* `<prefix>`: a CLI flag prefix based on the context (look at the parent configuration block to see which CLI flags prefix should be used)
2222

23-
Supported contents and default values of the config file:
23+
### Use environment variables in the configuration
24+
25+
You can use environment variable references in the config file to set values that need to be configurable during deployment.
26+
To do this, use:
27+
28+
```
29+
${VAR}
30+
```
31+
32+
Where VAR is the name of the environment variable.
33+
34+
Each variable reference is replaced at startup by the value of the environment variable.
35+
The replacement is case-sensitive and occurs before the YAML file is parsed.
36+
References to undefined variables are replaced by empty strings unless you specify a default value or custom error text.
37+
38+
To specify a default value, use:
39+
40+
```
41+
${VAR:default_value}
42+
```
43+
44+
Where default_value is the value to use if the environment variable is undefined.
45+
46+
### Supported contents and default values of the config file
2447

2548
```yaml
2649
# The Cortex service to run. Supported values are: all, distributor, ingester,

docs/configuration/config-file-reference.template

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Cortex can be configured using a YAML file - specified using the `-config.file`
1111

1212
To specify which configuration file to load, pass the `-config.file` flag at the command line. The file is written in [YAML format](https://en.wikipedia.org/wiki/YAML), defined by the scheme below. Brackets indicate that a parameter is optional.
1313

14-
Generic placeholders are defined as follows:
14+
### Generic placeholders
1515

1616
* `<boolean>`: a boolean that can take the values `true` or `false`
1717
* `<int>`: any integer matching the regular expression `[1-9]+[0-9]*`
@@ -20,5 +20,28 @@ Generic placeholders are defined as follows:
2020
* `<url>`: an URL
2121
* `<prefix>`: a CLI flag prefix based on the context (look at the parent configuration block to see which CLI flags prefix should be used)
2222

23-
Supported contents and default values of the config file:
23+
### Use environment variables in the configuration
24+
25+
You can use environment variable references in the config file to set values that need to be configurable during deployment.
26+
To do this, use:
27+
28+
```
29+
${VAR}
30+
```
31+
32+
Where VAR is the name of the environment variable.
33+
34+
Each variable reference is replaced at startup by the value of the environment variable.
35+
The replacement is case-sensitive and occurs before the YAML file is parsed.
36+
References to undefined variables are replaced by empty strings unless you specify a default value or custom error text.
37+
38+
To specify a default value, use:
39+
40+
```
41+
${VAR:default_value}
42+
```
43+
44+
Where default_value is the value to use if the environment variable is undefined.
45+
46+
### Supported contents and default values of the config file
2447

0 commit comments

Comments
 (0)