Add environment variable expansion in external label values

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
This commit is contained in:
Julien Pivotto 2021-03-25 22:28:58 +01:00
parent d680880b31
commit e635ca834b
6 changed files with 92 additions and 27 deletions

View file

@ -120,6 +120,7 @@ type flagConfig struct {
// for ease of use. // for ease of use.
enablePromQLAtModifier bool enablePromQLAtModifier bool
enablePromQLNegativeOffset bool enablePromQLNegativeOffset bool
enableExpandExternalLabels bool
prometheusURL string prometheusURL string
corsRegexString string corsRegexString string
@ -145,6 +146,9 @@ func (c *flagConfig) setFeatureListOptions(logger log.Logger) error {
case "remote-write-receiver": case "remote-write-receiver":
c.web.RemoteWriteReceiver = true c.web.RemoteWriteReceiver = true
level.Info(logger).Log("msg", "Experimental remote-write-receiver enabled") level.Info(logger).Log("msg", "Experimental remote-write-receiver enabled")
case "expand-external-labels":
c.enableExpandExternalLabels = true
level.Info(logger).Log("msg", "Experimental expand-external-labels enabled")
case "exemplar-storage": case "exemplar-storage":
c.tsdb.MaxExemplars = maxExemplars c.tsdb.MaxExemplars = maxExemplars
level.Info(logger).Log("msg", "Experimental in-memory exemplar storage enabled") level.Info(logger).Log("msg", "Experimental in-memory exemplar storage enabled")
@ -307,7 +311,7 @@ func main() {
a.Flag("query.max-samples", "Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return."). a.Flag("query.max-samples", "Maximum number of samples a single query can load into memory. Note that queries will fail if they try to load more samples than this into memory, so this also limits the number of samples a query can return.").
Default("50000000").IntVar(&cfg.queryMaxSamples) Default("50000000").IntVar(&cfg.queryMaxSamples)
a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-at-modifier, promql-negative-offset, remote-write-receiver, exemplar-storage. See https://prometheus.io/docs/prometheus/latest/disabled_features/ for more details."). a.Flag("enable-feature", "Comma separated feature names to enable. Valid options: promql-at-modifier, promql-negative-offset, remote-write-receiver, exemplar-storage, expand-external-labels. See https://prometheus.io/docs/prometheus/latest/disabled_features/ for more details.").
Default("").StringsVar(&cfg.featureList) Default("").StringsVar(&cfg.featureList)
promlogflag.AddFlags(a, &cfg.promlogConfig) promlogflag.AddFlags(a, &cfg.promlogConfig)
@ -343,7 +347,7 @@ func main() {
} }
// Throw error for invalid config before starting other components. // Throw error for invalid config before starting other components.
if _, err := config.LoadFile(cfg.configFile); err != nil { if _, err := config.LoadFile(cfg.configFile, false, log.NewNopLogger()); err != nil {
level.Error(logger).Log("msg", fmt.Sprintf("Error loading config (--config.file=%s)", cfg.configFile), "err", err) level.Error(logger).Log("msg", fmt.Sprintf("Error loading config (--config.file=%s)", cfg.configFile), "err", err)
os.Exit(2) os.Exit(2)
} }
@ -721,11 +725,11 @@ func main() {
for { for {
select { select {
case <-hup: case <-hup:
if err := reloadConfig(cfg.configFile, logger, noStepSubqueryInterval, reloaders...); err != nil { if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, logger, noStepSubqueryInterval, reloaders...); err != nil {
level.Error(logger).Log("msg", "Error reloading config", "err", err) level.Error(logger).Log("msg", "Error reloading config", "err", err)
} }
case rc := <-webHandler.Reload(): case rc := <-webHandler.Reload():
if err := reloadConfig(cfg.configFile, logger, noStepSubqueryInterval, reloaders...); err != nil { if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, logger, noStepSubqueryInterval, reloaders...); err != nil {
level.Error(logger).Log("msg", "Error reloading config", "err", err) level.Error(logger).Log("msg", "Error reloading config", "err", err)
rc <- err rc <- err
} else { } else {
@ -757,7 +761,7 @@ func main() {
return nil return nil
} }
if err := reloadConfig(cfg.configFile, logger, noStepSubqueryInterval, reloaders...); err != nil { if err := reloadConfig(cfg.configFile, cfg.enableExpandExternalLabels, logger, noStepSubqueryInterval, reloaders...); err != nil {
return errors.Wrapf(err, "error loading config from %q", cfg.configFile) return errors.Wrapf(err, "error loading config from %q", cfg.configFile)
} }
@ -938,7 +942,7 @@ type reloader struct {
reloader func(*config.Config) error reloader func(*config.Config) error
} }
func reloadConfig(filename string, logger log.Logger, noStepSuqueryInterval *safePromQLNoStepSubqueryInterval, rls ...reloader) (err error) { func reloadConfig(filename string, expandExternalLabels bool, logger log.Logger, noStepSuqueryInterval *safePromQLNoStepSubqueryInterval, rls ...reloader) (err error) {
start := time.Now() start := time.Now()
timings := []interface{}{} timings := []interface{}{}
level.Info(logger).Log("msg", "Loading configuration file", "filename", filename) level.Info(logger).Log("msg", "Loading configuration file", "filename", filename)
@ -952,7 +956,7 @@ func reloadConfig(filename string, logger log.Logger, noStepSuqueryInterval *saf
} }
}() }()
conf, err := config.LoadFile(filename) conf, err := config.LoadFile(filename, expandExternalLabels, logger)
if err != nil { if err != nil {
return errors.Wrapf(err, "couldn't load configuration (--config.file=%q)", filename) return errors.Wrapf(err, "couldn't load configuration (--config.file=%q)", filename)
} }

View file

@ -291,7 +291,7 @@ func checkFileExists(fn string) error {
func checkConfig(filename string) ([]string, error) { func checkConfig(filename string) ([]string, error) {
fmt.Println("Checking", filename) fmt.Println("Checking", filename)
cfg, err := config.LoadFile(filename) cfg, err := config.LoadFile(filename, false, log.NewNopLogger())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -17,11 +17,14 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/common/config" "github.com/prometheus/common/config"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
@ -60,7 +63,7 @@ var (
) )
// Load parses the YAML input s into a Config. // Load parses the YAML input s into a Config.
func Load(s string) (*Config, error) { func Load(s string, expandExternalLabels bool, logger log.Logger) (*Config, error) {
cfg := &Config{} cfg := &Config{}
// If the entire config body is empty the UnmarshalYAML method is // If the entire config body is empty the UnmarshalYAML method is
// never called. We thus have to set the DefaultConfig at the entry // never called. We thus have to set the DefaultConfig at the entry
@ -71,16 +74,35 @@ func Load(s string) (*Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !expandExternalLabels {
return cfg, nil
}
for i, v := range cfg.GlobalConfig.ExternalLabels {
newV := os.Expand(v.Value, func(s string) string {
if v := os.Getenv(s); v != "" {
return v
}
level.Warn(logger).Log("msg", "Empty environment variable", "name", s)
return ""
})
if newV != v.Value {
level.Debug(logger).Log("msg", "External label replaced", "label", v.Name, "input", v.Value, "output", newV)
v.Value = newV
cfg.GlobalConfig.ExternalLabels[i] = v
}
}
return cfg, nil return cfg, nil
} }
// LoadFile parses the given YAML file into a Config. // LoadFile parses the given YAML file into a Config.
func LoadFile(filename string) (*Config, error) { func LoadFile(filename string, expandExternalLabels bool, logger log.Logger) (*Config, error) {
content, err := ioutil.ReadFile(filename) content, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := Load(string(content)) cfg, err := Load(string(content), expandExternalLabels, logger)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "parsing YAML file %s", filename) return nil, errors.Wrapf(err, "parsing YAML file %s", filename)
} }

View file

@ -17,11 +17,13 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/go-kit/kit/log"
"github.com/prometheus/common/config" "github.com/prometheus/common/config"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -826,7 +828,7 @@ var expectedConf = &Config{
} }
func TestYAMLRoundtrip(t *testing.T) { func TestYAMLRoundtrip(t *testing.T) {
want, err := LoadFile("testdata/roundtrip.good.yml") want, err := LoadFile("testdata/roundtrip.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
out, err := yaml.Marshal(want) out, err := yaml.Marshal(want)
@ -839,7 +841,7 @@ func TestYAMLRoundtrip(t *testing.T) {
} }
func TestRemoteWriteRetryOnRateLimit(t *testing.T) { func TestRemoteWriteRetryOnRateLimit(t *testing.T) {
want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml") want, err := LoadFile("testdata/remote_write_retry_on_rate_limit.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
out, err := yaml.Marshal(want) out, err := yaml.Marshal(want)
@ -855,16 +857,16 @@ func TestRemoteWriteRetryOnRateLimit(t *testing.T) {
func TestLoadConfig(t *testing.T) { func TestLoadConfig(t *testing.T) {
// Parse a valid file that sets a global scrape timeout. This tests whether parsing // Parse a valid file that sets a global scrape timeout. This tests whether parsing
// an overwritten default field in the global config permanently changes the default. // an overwritten default field in the global config permanently changes the default.
_, err := LoadFile("testdata/global_timeout.good.yml") _, err := LoadFile("testdata/global_timeout.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
c, err := LoadFile("testdata/conf.good.yml") c, err := LoadFile("testdata/conf.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, expectedConf, c) require.Equal(t, expectedConf, c)
} }
func TestScrapeIntervalLarger(t *testing.T) { func TestScrapeIntervalLarger(t *testing.T) {
c, err := LoadFile("testdata/scrape_interval_larger.good.yml") c, err := LoadFile("testdata/scrape_interval_larger.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, len(c.ScrapeConfigs)) require.Equal(t, 1, len(c.ScrapeConfigs))
for _, sc := range c.ScrapeConfigs { for _, sc := range c.ScrapeConfigs {
@ -874,7 +876,7 @@ func TestScrapeIntervalLarger(t *testing.T) {
// YAML marshaling must not reveal authentication credentials. // YAML marshaling must not reveal authentication credentials.
func TestElideSecrets(t *testing.T) { func TestElideSecrets(t *testing.T) {
c, err := LoadFile("testdata/conf.good.yml") c, err := LoadFile("testdata/conf.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
secretRe := regexp.MustCompile(`\\u003csecret\\u003e|<secret>`) secretRe := regexp.MustCompile(`\\u003csecret\\u003e|<secret>`)
@ -891,26 +893,26 @@ func TestElideSecrets(t *testing.T) {
func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) { func TestLoadConfigRuleFilesAbsolutePath(t *testing.T) {
// Parse a valid file that sets a rule files with an absolute path // Parse a valid file that sets a rule files with an absolute path
c, err := LoadFile(ruleFilesConfigFile) c, err := LoadFile(ruleFilesConfigFile, false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, ruleFilesExpectedConf, c) require.Equal(t, ruleFilesExpectedConf, c)
} }
func TestKubernetesEmptyAPIServer(t *testing.T) { func TestKubernetesEmptyAPIServer(t *testing.T) {
_, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml") _, err := LoadFile("testdata/kubernetes_empty_apiserver.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
} }
func TestKubernetesSelectors(t *testing.T) { func TestKubernetesSelectors(t *testing.T) {
_, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml") _, err := LoadFile("testdata/kubernetes_selectors_endpoints.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
_, err = LoadFile("testdata/kubernetes_selectors_node.good.yml") _, err = LoadFile("testdata/kubernetes_selectors_node.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
_, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml") _, err = LoadFile("testdata/kubernetes_selectors_ingress.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
_, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml") _, err = LoadFile("testdata/kubernetes_selectors_pod.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
_, err = LoadFile("testdata/kubernetes_selectors_service.good.yml") _, err = LoadFile("testdata/kubernetes_selectors_service.good.yml", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
} }
@ -1173,7 +1175,7 @@ var expectedErrors = []struct {
func TestBadConfigs(t *testing.T) { func TestBadConfigs(t *testing.T) {
for _, ee := range expectedErrors { for _, ee := range expectedErrors {
_, err := LoadFile("testdata/" + ee.filename) _, err := LoadFile("testdata/"+ee.filename, false, log.NewNopLogger())
require.Error(t, err, "%s", ee.filename) require.Error(t, err, "%s", ee.filename)
require.Contains(t, err.Error(), ee.errMsg, require.Contains(t, err.Error(), ee.errMsg,
"Expected error for %s to contain %q but got: %s", ee.filename, ee.errMsg, err) "Expected error for %s to contain %q but got: %s", ee.filename, ee.errMsg, err)
@ -1197,14 +1199,38 @@ func TestBadStaticConfigsYML(t *testing.T) {
} }
func TestEmptyConfig(t *testing.T) { func TestEmptyConfig(t *testing.T) {
c, err := Load("") c, err := Load("", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
exp := DefaultConfig exp := DefaultConfig
require.Equal(t, exp, *c) require.Equal(t, exp, *c)
} }
func TestExpandExternalLabels(t *testing.T) {
// Cleanup ant TEST env variable that could exist on the system.
os.Setenv("TEST", "")
c, err := LoadFile("testdata/external_labels.good.yml", false, 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: "foo${TEST}bar"}, c.GlobalConfig.ExternalLabels[1])
require.Equal(t, labels.Label{Name: "foo", Value: "${TEST}"}, c.GlobalConfig.ExternalLabels[2])
c, err = LoadFile("testdata/external_labels.good.yml", 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])
os.Setenv("TEST", "TestValue")
c, err = LoadFile("testdata/external_labels.good.yml", 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: "fooTestValuebar"}, c.GlobalConfig.ExternalLabels[1])
require.Equal(t, labels.Label{Name: "foo", Value: "TestValue"}, c.GlobalConfig.ExternalLabels[2])
}
func TestEmptyGlobalBlock(t *testing.T) { func TestEmptyGlobalBlock(t *testing.T) {
c, err := Load("global:\n") c, err := Load("global:\n", false, log.NewNopLogger())
require.NoError(t, err) require.NoError(t, err)
exp := DefaultConfig exp := DefaultConfig
require.Equal(t, exp, *c) require.Equal(t, exp, *c)

View file

@ -0,0 +1,5 @@
global:
external_labels:
bar: foo
foo: ${TEST}
baz: foo${TEST}bar

View file

@ -18,6 +18,14 @@ They may be enabled by default in future versions.
The `@` modifier lets you specify the evaluation time for instant vector selectors, The `@` modifier lets you specify the evaluation time for instant vector selectors,
range vector selectors, and subqueries. More details can be found [here](querying/basics.md#modifier). range vector selectors, and subqueries. More details can be found [here](querying/basics.md#modifier).
## Expand environment variables in external labels
`--enable-feature=expand-external-labels`
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.
## Negative offset in PromQL ## Negative offset in PromQL
This negative offset is disabled by default since it breaks the invariant This negative offset is disabled by default since it breaks the invariant