From 2cd58e43a1ea8574de59756533ed8b2c27ea9c9e Mon Sep 17 00:00:00 2001 From: dcseifert Date: Wed, 9 Dec 2020 11:14:36 +0100 Subject: [PATCH 1/3] Added config environment expansion --- pkg/config/config.go | 4 +++- pkg/config/config_test.go | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index b9a6063e619c..118c76fc91f0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -71,7 +71,9 @@ func LoadFile(filename string, c *Config) error { return errors.Wrap(err, "error reading config file") } - return LoadBytes(buf, c) + // Expand from environment and load + expandedConfig := os.ExpandEnv(string(buf)) + return LoadBytes([]byte(expandedConfig), c) } // LoadBytes unmarshals a config from a buffer. Defaults are not diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f9bc93de4c45..f199361a2aef 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "flag" + "os" "testing" "time" @@ -49,6 +50,28 @@ prometheus: require.Equal(t, expect, c.Prometheus.Global) } +func TestConfig_OverrideByEnvironmentOnLoad(t *testing.T) { + cfg := ` +prometheus: + wal_directory: /tmp/wal + global: + scrape_timeout: ${SCRAPE_TIMEOUT}` + expect := promCfg.GlobalConfig{ + ScrapeInterval: model.Duration(1 * time.Minute), + ScrapeTimeout: model.Duration(33 * time.Second), + EvaluationInterval: model.Duration(1 * time.Minute), + } + _ = os.Setenv("SCRAPE_TIMEOUT", "33s") + + fs := flag.NewFlagSet("test", flag.ExitOnError) + c, err := load(fs, []string{"-config.file", "test"}, func(_ string, c *Config) error { + expandedConfig := os.ExpandEnv(cfg) + return LoadBytes([]byte(expandedConfig), c) + }) + require.NoError(t, err) + require.Equal(t, expect, c.Prometheus.Global) +} + func TestConfig_FlagsAreAccepted(t *testing.T) { cfg := ` prometheus: From e6718d8dc4bc7d6740ca317c7da306a57fec7ae3 Mon Sep 17 00:00:00 2001 From: dcseifert Date: Wed, 9 Dec 2020 17:32:58 +0100 Subject: [PATCH 2/3] Implemented optional env-var config substitution using CLI flag --- CHANGELOG.md | 2 + go.mod | 1 + go.sum | 2 + pkg/config/config.go | 31 +- pkg/config/config_test.go | 22 +- vendor/github.com/drone/envsubst/.drone.yml | 8 + vendor/github.com/drone/envsubst/.gitignore | 1 + vendor/github.com/drone/envsubst/LICENSE | 21 + vendor/github.com/drone/envsubst/README | 33 ++ vendor/github.com/drone/envsubst/eval.go | 19 + vendor/github.com/drone/envsubst/funcs.go | 228 +++++++++++ vendor/github.com/drone/envsubst/go.mod | 3 + vendor/github.com/drone/envsubst/go.sum | 2 + .../github.com/drone/envsubst/parse/node.go | 86 ++++ .../github.com/drone/envsubst/parse/parse.go | 374 ++++++++++++++++++ .../github.com/drone/envsubst/parse/scan.go | 280 +++++++++++++ .../github.com/drone/envsubst/path/match.go | 207 ++++++++++ vendor/github.com/drone/envsubst/template.go | 157 ++++++++ vendor/modules.txt | 4 + 19 files changed, 1459 insertions(+), 22 deletions(-) create mode 100644 vendor/github.com/drone/envsubst/.drone.yml create mode 100644 vendor/github.com/drone/envsubst/.gitignore create mode 100644 vendor/github.com/drone/envsubst/LICENSE create mode 100644 vendor/github.com/drone/envsubst/README create mode 100644 vendor/github.com/drone/envsubst/eval.go create mode 100644 vendor/github.com/drone/envsubst/funcs.go create mode 100644 vendor/github.com/drone/envsubst/go.mod create mode 100644 vendor/github.com/drone/envsubst/go.sum create mode 100644 vendor/github.com/drone/envsubst/parse/node.go create mode 100644 vendor/github.com/drone/envsubst/parse/parse.go create mode 100644 vendor/github.com/drone/envsubst/parse/scan.go create mode 100644 vendor/github.com/drone/envsubst/path/match.go create mode 100644 vendor/github.com/drone/envsubst/template.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b29e0eb891..bc61db4e7f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ this platform. FreeBSD builds will return in a future release. - [FEATURE] New integration: [consul_exporter](https://github.com/prometheus/consul_exporter) (@rfratto) + +- [FEATURE] Add optional environment variable substitution of configuration file. (@dcseifert) - [ENHANCEMENT] `min_wal_time` and `max_wal_time` have been added to the instance config settings, guaranteeing that data in the WAL will exist for at diff --git a/go.mod b/go.mod index 33c85c4b6fb8..4bbf85bea974 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( contrib.go.opencensus.io/exporter/prometheus v0.2.0 github.com/cortexproject/cortex v1.4.1-0.20201022071705-85942c5703cf + github.com/drone/envsubst v1.0.2 github.com/go-kit/kit v0.10.0 github.com/go-playground/validator/v10 v10.4.0 // indirect github.com/gogo/protobuf v1.3.1 diff --git a/go.sum b/go.sum index 4d984449bd7c..3a2c8512c868 100644 --- a/go.sum +++ b/go.sum @@ -376,6 +376,8 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/drone/envsubst v1.0.2 h1:dpYLMAspQHW0a8dZpLRKe9jCNvIGZPhCPrycZzIHdqo= +github.com/drone/envsubst v1.0.2/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= diff --git a/pkg/config/config.go b/pkg/config/config.go index 118c76fc91f0..119e08d40047 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" + "github.com/drone/envsubst" "github.com/grafana/agent/pkg/integrations" "github.com/grafana/agent/pkg/loki" "github.com/grafana/agent/pkg/prom" @@ -65,21 +66,27 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) { } // LoadFile reads a file and passes the contents to Load -func LoadFile(filename string, c *Config) error { +func LoadFile(filename string, expandEnvVars bool, c *Config) error { buf, err := ioutil.ReadFile(filename) if err != nil { return errors.Wrap(err, "error reading config file") } - - // Expand from environment and load - expandedConfig := os.ExpandEnv(string(buf)) - return LoadBytes([]byte(expandedConfig), c) + return LoadBytes(buf, expandEnvVars, c) } // LoadBytes unmarshals a config from a buffer. Defaults are not // applied to the file and must be done manually if LoadBytes // is called directly. -func LoadBytes(buf []byte, c *Config) error { +func LoadBytes(buf []byte, expandEnvVars bool, c *Config) error { + // (Optionally) expand with environment variables + if expandEnvVars { + s, err := envsubst.EvalEnv(string(buf)) + if err != nil { + return errors.Wrap(err, "unable to substitute config with environment variables") + } + buf = []byte(s) + } + // Unmarshal yaml config return yaml.UnmarshalStrict(buf, c) } @@ -92,15 +99,17 @@ func Load(fs *flag.FlagSet, args []string) (*Config, error) { // load allows for tests to inject a function for retreiving the config file that // doesn't require having a literal file on disk. -func load(fs *flag.FlagSet, args []string, loader func(string, *Config) error) (*Config, error) { +func load(fs *flag.FlagSet, args []string, loader func(string, bool, *Config) error) (*Config, error) { var ( - printVersion bool - cfg Config - file string + printVersion bool + cfg Config + file string + configExpandEnv bool ) fs.StringVar(&file, "config.file", "", "configuration file to load") fs.BoolVar(&printVersion, "version", false, "Print this build's version information") + fs.BoolVar(&configExpandEnv, "config.expand-env", false, "Expands ${var} in config according to the values of the environment variables.") cfg.RegisterFlags(fs) if err := fs.Parse(args); err != nil { @@ -114,7 +123,7 @@ func load(fs *flag.FlagSet, args []string, loader func(string, *Config) error) ( if file == "" { return nil, fmt.Errorf("-config.file flag required") - } else if err := loader(file, &cfg); err != nil { + } else if err := loader(file, configExpandEnv, &cfg); err != nil { return nil, fmt.Errorf("error loading config file %s: %w", file, err) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index f199361a2aef..456dbc996bd8 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -21,8 +21,8 @@ prometheus: scrape_timeout: 33s` fs := flag.NewFlagSet("test", flag.ExitOnError) - c, err := load(fs, []string{"-config.file", "test"}, func(_ string, c *Config) error { - return LoadBytes([]byte(cfg), c) + c, err := load(fs, []string{"-config.file", "test"}, func(_ string, _ bool, c *Config) error { + return LoadBytes([]byte(cfg), false, c) }) require.NoError(t, err) require.NotEmpty(t, c.Prometheus.ServiceConfig.Lifecycler.InfNames) @@ -43,8 +43,8 @@ prometheus: } fs := flag.NewFlagSet("test", flag.ExitOnError) - c, err := load(fs, []string{"-config.file", "test"}, func(_ string, c *Config) error { - return LoadBytes([]byte(cfg), c) + c, err := load(fs, []string{"-config.file", "test"}, func(_ string, _ bool, c *Config) error { + return LoadBytes([]byte(cfg), false, c) }) require.NoError(t, err) require.Equal(t, expect, c.Prometheus.Global) @@ -64,9 +64,8 @@ prometheus: _ = os.Setenv("SCRAPE_TIMEOUT", "33s") fs := flag.NewFlagSet("test", flag.ExitOnError) - c, err := load(fs, []string{"-config.file", "test"}, func(_ string, c *Config) error { - expandedConfig := os.ExpandEnv(cfg) - return LoadBytes([]byte(expandedConfig), c) + c, err := load(fs, []string{"-config.file", "test"}, func(_ string, _ bool, c *Config) error { + return LoadBytes([]byte(cfg), true, c) }) require.NoError(t, err) require.Equal(t, expect, c.Prometheus.Global) @@ -82,10 +81,11 @@ prometheus: args := []string{ "-config.file", "test", "-prometheus.wal-directory", "/tmp/wal", + "-config.expand-env", } - c, err := load(fs, args, func(_ string, c *Config) error { - return LoadBytes([]byte(cfg), c) + c, err := load(fs, args, func(_ string, _ bool, c *Config) error { + return LoadBytes([]byte(cfg), false, c) }) require.NoError(t, err) require.Equal(t, "/tmp/wal", c.Prometheus.WALDir) @@ -100,7 +100,7 @@ prometheus: scrape_timeout: 10s scrape_timeout: 15s` var c Config - err := LoadBytes([]byte(cfg), &c) + err := LoadBytes([]byte(cfg), false, &c) require.Error(t, err) }) @@ -111,7 +111,7 @@ prometheus: global: scrape_timeout: 10s` var c Config - err := LoadBytes([]byte(cfg), &c) + err := LoadBytes([]byte(cfg), false, &c) require.Error(t, err) }) } diff --git a/vendor/github.com/drone/envsubst/.drone.yml b/vendor/github.com/drone/envsubst/.drone.yml new file mode 100644 index 000000000000..2b062137f3bc --- /dev/null +++ b/vendor/github.com/drone/envsubst/.drone.yml @@ -0,0 +1,8 @@ +kind: pipeline +name: default + +steps: +- name: build + image: golang:1.11 + commands: + - go test -v ./... diff --git a/vendor/github.com/drone/envsubst/.gitignore b/vendor/github.com/drone/envsubst/.gitignore new file mode 100644 index 000000000000..2d830686d42d --- /dev/null +++ b/vendor/github.com/drone/envsubst/.gitignore @@ -0,0 +1 @@ +coverage.out diff --git a/vendor/github.com/drone/envsubst/LICENSE b/vendor/github.com/drone/envsubst/LICENSE new file mode 100644 index 000000000000..1de55b7f4f88 --- /dev/null +++ b/vendor/github.com/drone/envsubst/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 drone.io + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/drone/envsubst/README b/vendor/github.com/drone/envsubst/README new file mode 100644 index 000000000000..31c2ac58f578 --- /dev/null +++ b/vendor/github.com/drone/envsubst/README @@ -0,0 +1,33 @@ +Go package for expanding variables in a string using ${var} syntax. Includes support for bash string replacement functions. + +Documentation: + + http://godoc.org/github.com/drone/envsubst + +Supported Functions: + + ${var^} + ${var^^} + ${var,} + ${var,,} + ${var:position} + ${var:position:length} + ${var#substring} + ${var##substring} + ${var%substring} + ${var%%substring} + ${var/substring/replacement} + ${var//substring/replacement} + ${var/#substring/replacement} + ${var/%substring/replacement} + ${#var} + ${var=default} + ${var:=default} + ${var:-default} + +Unsupported Functions: + + ${var-default} + ${var+default} + ${var:?default} + ${var:+default} diff --git a/vendor/github.com/drone/envsubst/eval.go b/vendor/github.com/drone/envsubst/eval.go new file mode 100644 index 000000000000..375ca4c9fcba --- /dev/null +++ b/vendor/github.com/drone/envsubst/eval.go @@ -0,0 +1,19 @@ +package envsubst + +import "os" + +// Eval replaces ${var} in the string based on the mapping function. +func Eval(s string, mapping func(string) string) (string, error) { + t, err := Parse(s) + if err != nil { + return s, err + } + return t.Execute(mapping) +} + +// EvalEnv replaces ${var} in the string according to the values of the +// current environment variables. References to undefined variables are +// replaced by the empty string. +func EvalEnv(s string) (string, error) { + return Eval(s, os.Getenv) +} diff --git a/vendor/github.com/drone/envsubst/funcs.go b/vendor/github.com/drone/envsubst/funcs.go new file mode 100644 index 000000000000..94a3be1f23e9 --- /dev/null +++ b/vendor/github.com/drone/envsubst/funcs.go @@ -0,0 +1,228 @@ +package envsubst + +import ( + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/drone/envsubst/path" +) + +// defines a parameter substitution function. +type substituteFunc func(string, ...string) string + +// toLen returns the length of string s. +func toLen(s string, args ...string) string { + return strconv.Itoa(len(s)) +} + +// toLower returns a copy of the string s with all characters +// mapped to their lower case. +func toLower(s string, args ...string) string { + return strings.ToLower(s) +} + +// toUpper returns a copy of the string s with all characters +// mapped to their upper case. +func toUpper(s string, args ...string) string { + return strings.ToUpper(s) +} + +// toLowerFirst returns a copy of the string s with the first +// character mapped to its lower case. +func toLowerFirst(s string, args ...string) string { + if s == "" { + return s + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToLower(r)) + s[n:] +} + +// toUpperFirst returns a copy of the string s with the first +// character mapped to its upper case. +func toUpperFirst(s string, args ...string) string { + if s == "" { + return s + } + r, n := utf8.DecodeRuneInString(s) + return string(unicode.ToUpper(r)) + s[n:] +} + +// toDefault returns a copy of the string s if not empty, else +// returns a copy of the first string arugment. +func toDefault(s string, args ...string) string { + if len(s) == 0 && len(args) == 1 { + s = args[0] + } + return s +} + +// toSubstr returns a slice of the string s at the specified +// length and position. +func toSubstr(s string, args ...string) string { + if len(args) == 0 { + return s // should never happen + } + + pos, err := strconv.Atoi(args[0]) + if err != nil { + // bash returns the string if the position + // cannot be parsed. + return s + } + + if len(args) == 1 { + if pos < len(s) { + return s[pos:] + } + // if the position exceeds the length of the + // string an empty string is returned + return "" + } + + length, err := strconv.Atoi(args[1]) + if err != nil { + // bash returns the string if the length + // cannot be parsed. + return s + } + + if pos+length >= len(s) { + // if the position exceeds the length of the + // string just return the rest of it like bash + return s[pos:] + } + + return s[pos : pos+length] +} + +// replaceAll returns a copy of the string s with all instances +// of the substring replaced with the replacement string. +func replaceAll(s string, args ...string) string { + switch len(args) { + case 0: + return s + case 1: + return strings.Replace(s, args[0], "", -1) + default: + return strings.Replace(s, args[0], args[1], -1) + } +} + +// replaceFirst returns a copy of the string s with the first +// instance of the substring replaced with the replacement string. +func replaceFirst(s string, args ...string) string { + switch len(args) { + case 0: + return s + case 1: + return strings.Replace(s, args[0], "", 1) + default: + return strings.Replace(s, args[0], args[1], 1) + } +} + +// replacePrefix returns a copy of the string s with the matching +// prefix replaced with the replacement string. +func replacePrefix(s string, args ...string) string { + if len(args) != 2 { + return s + } + if strings.HasPrefix(s, args[0]) { + return strings.Replace(s, args[0], args[1], 1) + } + return s +} + +// replaceSuffix returns a copy of the string s with the matching +// suffix replaced with the replacement string. +func replaceSuffix(s string, args ...string) string { + if len(args) != 2 { + return s + } + if strings.HasSuffix(s, args[0]) { + s = strings.TrimSuffix(s, args[0]) + s = s + args[1] + } + return s +} + +// TODO + +func trimShortestPrefix(s string, args ...string) string { + if len(args) != 0 { + s = trimShortest(s, args[0]) + } + return s +} + +func trimShortestSuffix(s string, args ...string) string { + if len(args) != 0 { + r := reverse(s) + rarg := reverse(args[0]) + s = reverse(trimShortest(r, rarg)) + } + return s +} + +func trimLongestPrefix(s string, args ...string) string { + if len(args) != 0 { + s = trimLongest(s, args[0]) + } + return s +} + +func trimLongestSuffix(s string, args ...string) string { + if len(args) != 0 { + r := reverse(s) + rarg := reverse(args[0]) + s = reverse(trimLongest(r, rarg)) + } + return s +} + +func trimShortest(s, arg string) string { + var shortestMatch string + for i := 0; i < len(s); i++ { + match, err := path.Match(arg, s[0:len(s)-i]) + + if err != nil { + return s + } + + if match { + shortestMatch = s[0 : len(s)-i] + } + } + + if shortestMatch != "" { + return strings.TrimPrefix(s, shortestMatch) + } + + return s +} + +func trimLongest(s, arg string) string { + for i := 0; i < len(s); i++ { + match, err := path.Match(arg, s[0:len(s)-i]) + + if err != nil { + return s + } + + if match { + return strings.TrimPrefix(s, s[0:len(s)-i]) + } + } + + return s +} + +func reverse(s string) string { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r) +} diff --git a/vendor/github.com/drone/envsubst/go.mod b/vendor/github.com/drone/envsubst/go.mod new file mode 100644 index 000000000000..7b2ba94b6fe7 --- /dev/null +++ b/vendor/github.com/drone/envsubst/go.mod @@ -0,0 +1,3 @@ +module github.com/drone/envsubst + +require github.com/google/go-cmp v0.2.0 diff --git a/vendor/github.com/drone/envsubst/go.sum b/vendor/github.com/drone/envsubst/go.sum new file mode 100644 index 000000000000..5f4f6368972c --- /dev/null +++ b/vendor/github.com/drone/envsubst/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= diff --git a/vendor/github.com/drone/envsubst/parse/node.go b/vendor/github.com/drone/envsubst/parse/node.go new file mode 100644 index 000000000000..09787eb92db9 --- /dev/null +++ b/vendor/github.com/drone/envsubst/parse/node.go @@ -0,0 +1,86 @@ +package parse + +// Node is an element in the parse tree. +type Node interface { + node() +} + +// empty string node +var empty = new(TextNode) + +// a template is represented by a tree consisting of one +// or more of the following nodes. +type ( + // TextNode represents a string of text. + TextNode struct { + Value string + } + + // FuncNode represents a string function. + FuncNode struct { + Param string + Name string + Args []Node + } + + // ListNode represents a list of nodes. + ListNode struct { + Nodes []Node + } + + // ParamNode struct{ + // Name string + // } + // + // CaseNode struct { + // Name string + // First bool + // } + // + // LowerNode struct { + // Name string + // First bool + // } + // + // SubstrNode struct { + // Name string + // Pos Node + // Len Node + // } + // + // ReplaceNode struct { + // Name string + // Substring Node + // Replacement Node + // } + // + // TrimNode struct{ + // + // } + // + // DefaultNode struct { + // Name string + // Default Node + // } +) + +// newTextNode returns a new TextNode. +func newTextNode(text string) *TextNode { + return &TextNode{Value: text} +} + +// newListNode returns a new ListNode. +func newListNode(nodes ...Node) *ListNode { + return &ListNode{Nodes: nodes} +} + +// newFuncNode returns a new FuncNode. +func newFuncNode(name string) *FuncNode { + return &FuncNode{Param: name} +} + +// node() defines the node in a parse tree + +func (*TextNode) node() {} +func (*ListNode) node() {} +func (*FuncNode) node() {} diff --git a/vendor/github.com/drone/envsubst/parse/parse.go b/vendor/github.com/drone/envsubst/parse/parse.go new file mode 100644 index 000000000000..683b44d54263 --- /dev/null +++ b/vendor/github.com/drone/envsubst/parse/parse.go @@ -0,0 +1,374 @@ +package parse + +import "errors" + +// ErrBadSubstitution represents a substitution parsing error. +var ErrBadSubstitution = errors.New("bad substitution") + +// Tree is the representation of a single parsed SQL statement. +type Tree struct { + Root Node + + // Parsing only; cleared after parse. + scanner *scanner +} + +// Parse parses the string and returns a Tree. +func Parse(buf string) (*Tree, error) { + t := new(Tree) + t.scanner = new(scanner) + return t.Parse(buf) +} + +// Parse parses the string buffer to construct an ast +// representation for expansion. +func (t *Tree) Parse(buf string) (tree *Tree, err error) { + t.scanner.init(buf) + t.Root, err = t.parseAny() + return t, err +} + +func (t *Tree) parseAny() (Node, error) { + t.scanner.accept = acceptRune + t.scanner.mode = scanIdent | scanLbrack | scanEscape + + switch t.scanner.scan() { + case tokenIdent: + left := newTextNode( + t.scanner.string(), + ) + right, err := t.parseAny() + switch { + case err != nil: + return nil, err + case right == empty: + return left, nil + } + return newListNode(left, right), nil + case tokenEOF: + return empty, nil + case tokenLbrack: + left, err := t.parseFunc() + if err != nil { + return nil, err + } + + right, err := t.parseAny() + switch { + case err != nil: + return nil, err + case right == empty: + return left, nil + } + return newListNode(left, right), nil + } + + return nil, ErrBadSubstitution +} + +func (t *Tree) parseFunc() (Node, error) { + switch t.scanner.peek() { + case '#': + return t.parseLenFunc() + } + + var name string + t.scanner.accept = acceptIdent + t.scanner.mode = scanIdent + + switch t.scanner.scan() { + case tokenIdent: + name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + switch t.scanner.peek() { + case ':': + return t.parseDefaultOrSubstr(name) + case '=': + return t.parseDefaultFunc(name) + case ',', '^': + return t.parseCasingFunc(name) + case '/': + return t.parseReplaceFunc(name) + case '#': + return t.parseRemoveFunc(name, acceptHashFunc) + case '%': + return t.parseRemoveFunc(name, acceptPercentFunc) + } + + t.scanner.accept = acceptIdent + t.scanner.mode = scanRbrack + switch t.scanner.scan() { + case tokenRbrack: + return newFuncNode(name), nil + default: + return nil, ErrBadSubstitution + } +} + +// parse a substitution function parameter. +func (t *Tree) parseParam(accept acceptFunc, mode byte) (Node, error) { + t.scanner.accept = accept + t.scanner.mode = mode | scanLbrack + switch t.scanner.scan() { + case tokenLbrack: + return t.parseFunc() + case tokenIdent: + return newTextNode( + t.scanner.string(), + ), nil + default: + return nil, ErrBadSubstitution + } +} + +// parse either a default or substring substitution function. +func (t *Tree) parseDefaultOrSubstr(name string) (Node, error) { + t.scanner.read() + r := t.scanner.peek() + t.scanner.unread() + switch r { + case '=', '-', '?', '+': + return t.parseDefaultFunc(name) + default: + return t.parseSubstrFunc(name) + } +} + +// parses the ${param:offset} string function +// parses the ${param:offset:length} string function +func (t *Tree) parseSubstrFunc(name string) (Node, error) { + node := new(FuncNode) + node.Param = name + + t.scanner.accept = acceptOneColon + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + // scan arg[1] + { + param, err := t.parseParam(rejectColonClose, scanIdent) + if err != nil { + return nil, err + } + + // param.Value = t.scanner.string() + node.Args = append(node.Args, param) + } + + // expect delimiter or close + t.scanner.accept = acceptColon + t.scanner.mode = scanIdent | scanRbrack + switch t.scanner.scan() { + case tokenRbrack: + return node, nil + case tokenIdent: + // no-op + default: + return nil, ErrBadSubstitution + } + + // scan arg[2] + { + param, err := t.parseParam(acceptNotClosing, scanIdent) + if err != nil { + return nil, err + } + node.Args = append(node.Args, param) + } + + return node, t.consumeRbrack() +} + +// parses the ${param%word} string function +// parses the ${param%%word} string function +// parses the ${param#word} string function +// parses the ${param##word} string function +func (t *Tree) parseRemoveFunc(name string, accept acceptFunc) (Node, error) { + node := new(FuncNode) + node.Param = name + + t.scanner.accept = accept + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + // scan arg[1] + { + param, err := t.parseParam(acceptNotClosing, scanIdent) + if err != nil { + return nil, err + } + + // param.Value = t.scanner.string() + node.Args = append(node.Args, param) + } + + return node, t.consumeRbrack() +} + +// parses the ${param/pattern/string} string function +// parses the ${param//pattern/string} string function +// parses the ${param/#pattern/string} string function +// parses the ${param/%pattern/string} string function +func (t *Tree) parseReplaceFunc(name string) (Node, error) { + node := new(FuncNode) + node.Param = name + + t.scanner.accept = acceptReplaceFunc + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + // scan arg[1] + { + param, err := t.parseParam(acceptNotSlash, scanIdent|scanEscape) + if err != nil { + return nil, err + } + node.Args = append(node.Args, param) + } + + // expect delimiter + t.scanner.accept = acceptSlash + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + // no-op + default: + return nil, ErrBadSubstitution + } + + // check for blank string + switch t.scanner.peek() { + case '}': + return node, t.consumeRbrack() + } + + // scan arg[2] + { + param, err := t.parseParam(acceptNotClosing, scanIdent|scanEscape) + if err != nil { + return nil, err + } + node.Args = append(node.Args, param) + } + + return node, t.consumeRbrack() +} + +// parses the ${parameter=word} string function +// parses the ${parameter:=word} string function +// parses the ${parameter:-word} string function +// parses the ${parameter:?word} string function +// parses the ${parameter:+word} string function +func (t *Tree) parseDefaultFunc(name string) (Node, error) { + node := new(FuncNode) + node.Param = name + + t.scanner.accept = acceptDefaultFunc + if t.scanner.peek() == '=' { + t.scanner.accept = acceptOneEqual + } + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + // scan arg[1] + { + param, err := t.parseParam(acceptNotClosing, scanIdent) + if err != nil { + return nil, err + } + + // param.Value = t.scanner.string() + node.Args = append(node.Args, param) + } + + return node, t.consumeRbrack() +} + +// parses the ${param,} string function +// parses the ${param,,} string function +// parses the ${param^} string function +// parses the ${param^^} string function +func (t *Tree) parseCasingFunc(name string) (Node, error) { + node := new(FuncNode) + node.Param = name + + t.scanner.accept = acceptCasingFunc + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + return node, t.consumeRbrack() +} + +// parses the ${#param} string function +func (t *Tree) parseLenFunc() (Node, error) { + node := new(FuncNode) + + t.scanner.accept = acceptOneHash + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Name = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + t.scanner.accept = acceptIdent + t.scanner.mode = scanIdent + switch t.scanner.scan() { + case tokenIdent: + node.Param = t.scanner.string() + default: + return nil, ErrBadSubstitution + } + + return node, t.consumeRbrack() +} + +// consumeRbrack consumes a right closing bracket. If a closing +// bracket token is not consumed an ErrBadSubstitution is returned. +func (t *Tree) consumeRbrack() error { + t.scanner.mode = scanRbrack + if t.scanner.scan() != tokenRbrack { + return ErrBadSubstitution + } + return nil +} + +// consumeDelimiter consumes a function argument delimiter. If a +// delimiter is not consumed an ErrBadSubstitution is returned. +// func (t *Tree) consumeDelimiter(accept acceptFunc, mode uint) error { +// t.scanner.accept = accept +// t.scanner.mode = mode +// if t.scanner.scan() != tokenRbrack { +// return ErrBadSubstitution +// } +// return nil +// } diff --git a/vendor/github.com/drone/envsubst/parse/scan.go b/vendor/github.com/drone/envsubst/parse/scan.go new file mode 100644 index 000000000000..94812c160112 --- /dev/null +++ b/vendor/github.com/drone/envsubst/parse/scan.go @@ -0,0 +1,280 @@ +package parse + +import ( + "unicode" + "unicode/utf8" +) + +// eof rune sent when end of file is reached +var eof = rune(0) + +// token is a lexical token. +type token uint + +// list of lexical tokens. +const ( + // special tokens + tokenIllegal token = iota + tokenEOF + + // identifiers and literals + tokenIdent + + // operators and delimiters + tokenLbrack + tokenRbrack + tokenQuote +) + +// predefined mode bits to control recognition of tokens. +const ( + scanIdent byte = 1 << iota + scanLbrack + scanRbrack + scanEscape +) + +// returns true if rune is accepted. +type acceptFunc func(r rune, i int) bool + +// scanner implements a lexical scanner that reads unicode +// characters and tokens from a string buffer. +type scanner struct { + buf string + pos int + start int + width int + mode byte + + accept acceptFunc +} + +// init initializes a scanner with a new buffer. +func (s *scanner) init(buf string) { + s.buf = buf + s.pos = 0 + s.start = 0 + s.width = 0 + s.accept = nil +} + +// read returns the next unicode character. It returns eof at +// the end of the string buffer. +func (s *scanner) read() rune { + if s.pos >= len(s.buf) { + s.width = 0 + return eof + } + r, w := utf8.DecodeRuneInString(s.buf[s.pos:]) + s.width = w + s.pos += s.width + return r +} + +func (s *scanner) unread() { + s.pos -= s.width +} + +// skip skips over the curring unicode character in the buffer +// by slicing and removing from the buffer. +func (s *scanner) skip() { + l := s.buf[:s.pos-1] + r := s.buf[s.pos:] + s.buf = l + r +} + +// peek returns the next unicode character in the buffer without +// advancing the scanner. It returns eof if the scanner's position +// is at the last character of the source. +func (s *scanner) peek() rune { + r := s.read() + s.unread() + return r +} + +// string returns the string corresponding to the most recently +// scanned token. Valid after calling scan(). +func (s *scanner) string() string { + return s.buf[s.start:s.pos] +} + +// scan reads the next token or Unicode character from source and +// returns it. It returns EOF at the end of the source. +func (s *scanner) scan() token { + s.start = s.pos + r := s.read() + switch { + case r == eof: + return tokenEOF + case s.scanLbrack(r): + return tokenLbrack + case s.scanRbrack(r): + return tokenRbrack + case s.scanIdent(r): + return tokenIdent + } + return tokenIllegal +} + +// scanIdent reads the next token or Unicode character from source +// and returns true if the Ident character is accepted. +func (s *scanner) scanIdent(r rune) bool { + if s.mode&scanIdent == 0 { + return false + } + if s.scanEscaped(r) { + s.skip() + } else if !s.accept(r, s.pos-s.start) { + return false + } +loop: + for { + r := s.read() + switch { + case r == eof: + s.unread() + break loop + case s.scanLbrack(r): + s.unread() + s.unread() + break loop + } + if s.scanEscaped(r) { + s.skip() + continue + } + if !s.accept(r, s.pos-s.start) { + s.unread() + break loop + } + } + return true +} + +// scanLbrack reads the next token or Unicode character from source +// and returns true if the open bracket is encountered. +func (s *scanner) scanLbrack(r rune) bool { + if s.mode&scanLbrack == 0 { + return false + } + if r == '$' { + if s.read() == '{' { + return true + } + s.unread() + } + return false +} + +// scanRbrack reads the next token or Unicode character from source +// and returns true if the closing bracket is encountered. +func (s *scanner) scanRbrack(r rune) bool { + if s.mode&scanRbrack == 0 { + return false + } + return r == '}' +} + +// scanEscaped reads the next token or Unicode character from source +// and returns true if it being escaped and should be sipped. +func (s *scanner) scanEscaped(r rune) bool { + if s.mode&scanEscape == 0 { + return false + } + if r == '$' { + if s.peek() == '$' { + return true + } + } + if r != '\\' { + return false + } + switch s.peek() { + case '/', '\\': + return true + default: + return false + } +} + +// +// scanner functions accept or reject runes. +// + +func acceptRune(r rune, i int) bool { + return true +} + +func acceptIdent(r rune, i int) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' +} + +func acceptColon(r rune, i int) bool { + return r == ':' +} + +func acceptOneHash(r rune, i int) bool { + return r == '#' && i == 1 +} + +func acceptNone(r rune, i int) bool { + return false +} + +func acceptNotClosing(r rune, i int) bool { + return r != '}' +} + +func acceptHashFunc(r rune, i int) bool { + return r == '#' && i < 3 +} + +func acceptPercentFunc(r rune, i int) bool { + return r == '%' && i < 3 +} + +func acceptDefaultFunc(r rune, i int) bool { + switch { + case i == 1 && r == ':': + return true + case i == 2 && (r == '=' || r == '-' || r == '?' || r == '+'): + return true + default: + return false + } +} + +func acceptReplaceFunc(r rune, i int) bool { + switch { + case i == 1 && r == '/': + return true + case i == 2 && (r == '/' || r == '#' || r == '%'): + return true + default: + return false + } +} + +func acceptOneEqual(r rune, i int) bool { + return i == 1 && r == '=' +} + +func acceptOneColon(r rune, i int) bool { + return i == 1 && r == ':' +} + +func rejectColonClose(r rune, i int) bool { + return r != ':' && r != '}' +} + +func acceptSlash(r rune, i int) bool { + return r == '/' +} + +func acceptNotSlash(r rune, i int) bool { + return r != '/' +} + +func acceptCasingFunc(r rune, i int) bool { + return (r == ',' || r == '^') && i < 3 +} diff --git a/vendor/github.com/drone/envsubst/path/match.go b/vendor/github.com/drone/envsubst/path/match.go new file mode 100644 index 000000000000..9306b0c9d66f --- /dev/null +++ b/vendor/github.com/drone/envsubst/path/match.go @@ -0,0 +1,207 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package path + +import ( + "errors" + "unicode/utf8" +) + +// ErrBadPattern indicates a globbing pattern was malformed. +var ErrBadPattern = errors.New("syntax error in pattern") + +// Match reports whether name matches the shell file name pattern. +// The pattern syntax is: +// +// pattern: +// { term } +// term: +// '*' matches any sequence of non-/ characters +// '?' matches any single non-/ character +// '[' [ '^' ] { character-range } ']' +// character class (must be non-empty) +// c matches character c (c != '*', '?', '\\', '[') +// '\\' c matches character c +// +// character-range: +// c matches character c (c != '\\', '-', ']') +// '\\' c matches character c +// lo '-' hi matches character c for lo <= c <= hi +// +// Match requires pattern to match all of name, not just a substring. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +// +func Match(pattern, name string) (matched bool, err error) { +Pattern: + for len(pattern) > 0 { + var star bool + var chunk string + star, chunk, pattern = scanChunk(pattern) + if star && chunk == "" { + // Trailing * matches rest of string unless it has a /. + // return !strings.Contains(name, "/"), nil + + // Return rest of string + return true, nil + } + // Look for match at current position. + t, ok, err := matchChunk(chunk, name) + // if we're the last chunk, make sure we've exhausted the name + // otherwise we'll give a false result even if we could still match + // using the star + if ok && (len(t) == 0 || len(pattern) > 0) { + name = t + continue + } + if err != nil { + return false, err + } + if star { + // Look for match skipping i+1 bytes. + for i := 0; i < len(name); i++ { + t, ok, err := matchChunk(chunk, name[i+1:]) + if ok { + // if we're the last chunk, make sure we exhausted the name + if len(pattern) == 0 && len(t) > 0 { + continue + } + name = t + continue Pattern + } + if err != nil { + return false, err + } + } + } + return false, nil + } + return len(name) == 0, nil +} + +// scanChunk gets the next segment of pattern, which is a non-star string +// possibly preceded by a star. +func scanChunk(pattern string) (star bool, chunk, rest string) { + for len(pattern) > 0 && pattern[0] == '*' { + pattern = pattern[1:] + star = true + } + inrange := false + var i int +Scan: + for i = 0; i < len(pattern); i++ { + switch pattern[i] { + case '\\': + // error check handled in matchChunk: bad pattern. + if i+1 < len(pattern) { + i++ + } + case '[': + inrange = true + case ']': + inrange = false + case '*': + if !inrange { + break Scan + } + } + } + return star, pattern[0:i], pattern[i:] +} + +// matchChunk checks whether chunk matches the beginning of s. +// If so, it returns the remainder of s (after the match). +// Chunk is all single-character operators: literals, char classes, and ?. +func matchChunk(chunk, s string) (rest string, ok bool, err error) { + for len(chunk) > 0 { + if len(s) == 0 { + return + } + switch chunk[0] { + case '[': + // character class + r, n := utf8.DecodeRuneInString(s) + s = s[n:] + chunk = chunk[1:] + // possibly negated + notNegated := true + if len(chunk) > 0 && chunk[0] == '^' { + notNegated = false + chunk = chunk[1:] + } + // parse all ranges + match := false + nrange := 0 + for { + if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { + chunk = chunk[1:] + break + } + var lo, hi rune + if lo, chunk, err = getEsc(chunk); err != nil { + return + } + hi = lo + if chunk[0] == '-' { + if hi, chunk, err = getEsc(chunk[1:]); err != nil { + return + } + } + if lo <= r && r <= hi { + match = true + } + nrange++ + } + if match != notNegated { + return + } + + case '?': + _, n := utf8.DecodeRuneInString(s) + s = s[n:] + chunk = chunk[1:] + + case '\\': + chunk = chunk[1:] + if len(chunk) == 0 { + err = ErrBadPattern + return + } + fallthrough + + default: + if chunk[0] != s[0] { + return + } + s = s[1:] + chunk = chunk[1:] + } + } + return s, true, nil +} + +// getEsc gets a possibly-escaped character from chunk, for a character class. +func getEsc(chunk string) (r rune, nchunk string, err error) { + if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { + err = ErrBadPattern + return + } + if chunk[0] == '\\' { + chunk = chunk[1:] + if len(chunk) == 0 { + err = ErrBadPattern + return + } + } + r, n := utf8.DecodeRuneInString(chunk) + if r == utf8.RuneError && n == 1 { + err = ErrBadPattern + } + nchunk = chunk[n:] + if len(nchunk) == 0 { + err = ErrBadPattern + } + return +} diff --git a/vendor/github.com/drone/envsubst/template.go b/vendor/github.com/drone/envsubst/template.go new file mode 100644 index 000000000000..b725d4f62a36 --- /dev/null +++ b/vendor/github.com/drone/envsubst/template.go @@ -0,0 +1,157 @@ +package envsubst + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/drone/envsubst/parse" +) + +// state represents the state of template execution. It is not part of the +// template so that multiple executions can run in parallel. +type state struct { + template *Template + writer io.Writer + node parse.Node // current node + + // maps variable names to values + mapper func(string) string +} + +// Template is the representation of a parsed shell format string. +type Template struct { + tree *parse.Tree +} + +// Parse creates a new shell format template and parses the template +// definition from string s. +func Parse(s string) (t *Template, err error) { + t = new(Template) + t.tree, err = parse.Parse(s) + if err != nil { + return nil, err + } + return t, nil +} + +// ParseFile creates a new shell format template and parses the template +// definition from the named file. +func ParseFile(path string) (*Template, error) { + b, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + return Parse(string(b)) +} + +// Execute applies a parsed template to the specified data mapping. +func (t *Template) Execute(mapping func(string) string) (str string, err error) { + b := new(bytes.Buffer) + s := new(state) + s.node = t.tree.Root + s.mapper = mapping + s.writer = b + err = t.eval(s) + if err != nil { + return + } + return b.String(), nil +} + +func (t *Template) eval(s *state) (err error) { + switch node := s.node.(type) { + case *parse.TextNode: + err = t.evalText(s, node) + case *parse.FuncNode: + err = t.evalFunc(s, node) + case *parse.ListNode: + err = t.evalList(s, node) + } + return err +} + +func (t *Template) evalText(s *state, node *parse.TextNode) error { + _, err := io.WriteString(s.writer, node.Value) + return err +} + +func (t *Template) evalList(s *state, node *parse.ListNode) (err error) { + for _, n := range node.Nodes { + s.node = n + err = t.eval(s) + if err != nil { + return err + } + } + return nil +} + +func (t *Template) evalFunc(s *state, node *parse.FuncNode) error { + var w = s.writer + var buf bytes.Buffer + var args []string + for _, n := range node.Args { + buf.Reset() + s.writer = &buf + s.node = n + err := t.eval(s) + if err != nil { + return err + } + args = append(args, buf.String()) + } + + // restore the origin writer + s.writer = w + s.node = node + + v := s.mapper(node.Param) + + fn := lookupFunc(node.Name, len(args)) + + _, err := io.WriteString(s.writer, fn(v, args...)) + return err +} + +// lookupFunc returns the parameters substitution function by name. If the +// named function does not exists, a default function is returned. +func lookupFunc(name string, args int) substituteFunc { + switch name { + case ",": + return toLowerFirst + case ",,": + return toLower + case "^": + return toUpperFirst + case "^^": + return toUpper + case "#": + if args == 0 { + return toLen + } + return trimShortestPrefix + case "##": + return trimLongestPrefix + case "%": + return trimShortestSuffix + case "%%": + return trimLongestSuffix + case ":": + return toSubstr + case "/#": + return replacePrefix + case "/%": + return replaceSuffix + case "/": + return replaceFirst + case "//": + return replaceAll + case "=", ":=", ":-": + return toDefault + case ":?", ":+", "-", "+": + return toDefault + default: + return toDefault + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 905e59a7a244..0166a4a69512 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -172,6 +172,10 @@ github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig # github.com/docker/go-units v0.4.0 github.com/docker/go-units +# github.com/drone/envsubst v1.0.2 +github.com/drone/envsubst +github.com/drone/envsubst/parse +github.com/drone/envsubst/path # github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize # github.com/edsrzf/mmap-go v1.0.0 From f62db263a2f9bf7caa956cb30f05b0b98ec95f46 Mon Sep 17 00:00:00 2001 From: Daniel Seifert <53233799+dcseifert@users.noreply.github.com> Date: Wed, 9 Dec 2020 17:53:20 +0100 Subject: [PATCH 3/3] Update pkg/config/config.go Co-authored-by: Robert Fratto --- pkg/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 119e08d40047..edd690fe3f50 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -82,7 +82,7 @@ func LoadBytes(buf []byte, expandEnvVars bool, c *Config) error { if expandEnvVars { s, err := envsubst.EvalEnv(string(buf)) if err != nil { - return errors.Wrap(err, "unable to substitute config with environment variables") + return fmt.Errorf("unable to substitute config with environment variables: %w", err) } buf = []byte(s) }