diff --git a/go.mod b/go.mod index 1f18768237f..baec603afcd 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/securego/gosec/v2 v2.18.2 github.com/segmentio/backo-go v1.0.1 - github.com/sethvargo/go-envconfig v0.9.0 + github.com/sethvargo/go-envconfig v1.0.0 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 8dc63fa04a6..84f318473ca 100644 --- a/go.sum +++ b/go.sum @@ -1137,8 +1137,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= -github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/sethvargo/go-envconfig v1.0.0 h1:1C66wzy4QrROf5ew4KdVw942CQDa55qmlYmw9FZxZdU= +github.com/sethvargo/go-envconfig v1.0.0/go.mod h1:Lzc75ghUn5ucmcRGIdGQ33DKJrcjk4kihFYgSTBmjIc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= diff --git a/pkg/config/config.go b/pkg/config/config.go index 4eda636507e..f40597b266d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -35,7 +35,11 @@ func GetConfiguration() (*Configuration, error) { // typically in unit tests. func GetConfigurationWith(lookuper envconfig.Lookuper) (*Configuration, error) { var s Configuration - err := envconfig.ProcessWith(context.Background(), &s, lookuper) + c := envconfig.Config{ + Target: &s, + Lookuper: lookuper, + } + err := envconfig.ProcessWith(context.Background(), &c) if err != nil { return nil, err } diff --git a/vendor/github.com/sethvargo/go-envconfig/README.md b/vendor/github.com/sethvargo/go-envconfig/README.md index 51f0f8e674f..fa9fbfe5913 100644 --- a/vendor/github.com/sethvargo/go-envconfig/README.md +++ b/vendor/github.com/sethvargo/go-envconfig/README.md @@ -1,23 +1,19 @@ # Envconfig -[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/sethvargo/go-envconfig) -[![GitHub Actions](https://img.shields.io/github/workflow/status/sethvargo/go-envconfig/unit/main?style=flat-square)](https://github.com/sethvargo/go-envconfig/actions?query=branch%3Amain+-event%3Aschedule) +[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)][godoc] Envconfig populates struct field values based on environment variables or arbitrary lookup functions. It supports pre-setting mutations, which is useful for things like converting values to uppercase, trimming whitespace, or looking up secrets. -**Note:** Versions prior to v0.2 used a different import path. This README and -examples are for v0.2+. - ## Usage Define a struct with fields using the `env` tag: ```go type MyConfig struct { - Port int `env:"PORT"` + Port string `env:"PORT"` Username string `env:"USERNAME"` } ``` @@ -63,113 +59,141 @@ type MyConfig struct { } type DatabaseConfig struct { - Port int `env:"PORT"` + Port string `env:"PORT"` Username string `env:"USERNAME"` } ``` ## Configuration -Use the `env` struct tag to define configuration. +Use the `env` struct tag to define configuration. See the [godoc][] for usage +examples. -### Required +- `required` - marks a field as required. If a field is required, decoding + will error if the environment variable is unset. -If a field is required, processing will error if the environment variable is -unset. + ```go + type MyStruct struct { + Port string `env:"PORT, required"` + } + ``` -```go -type MyStruct struct { - Port int `env:"PORT,required"` -} -``` +- `default` - sets the default value for the environment variable is not set. + The environment variable must not be set (e.g. `unset PORT`). If the + environment variable is the empty string, envconfig considers that a "value" + and the default will **not** be used. -It is invalid to have a field as both `required` and `default`. + You can also set the default value to the value from another field or a + value from a different environment variable. -### Default + ```go + type MyStruct struct { + Port string `env:"PORT, default=5555"` + User string `env:"USER, default=$CURRENT_USER"` + } + ``` -If an environment variable is not set, the field will be set to the default -value. Note that the environment variable must not be set (e.g. `unset PORT`). -If the environment variable is the empty string, that counts as a "value" and -the default will not be used. +- `prefix` - sets the prefix to use for looking up environment variable keys + on child structs and fields. This is useful for shared configurations: -```go -type MyStruct struct { - Port int `env:"PORT,default=5555"` -} -``` + ```go + type RedisConfig struct { + Host string `env:"REDIS_HOST"` + User string `env:"REDIS_USER"` + } -You can also set the default value to another field or value from the -environment, for example: + type ServerConfig struct { + // CacheConfig will process values from $CACHE_REDIS_HOST and + // $CACHE_REDIS respectively. + CacheConfig *RedisConfig `env:", prefix=CACHE_"` -```go -type MyStruct struct { - DefaultPort int `env:"DEFAULT_PORT,default=5555"` - Port int `env:"OVERRIDE_PORT,default=$DEFAULT_PORT"` -} -``` + // RateLimitConfig will process values from $RATE_LIMIT_REDIS_HOST and + // $RATE_LIMIT_REDIS respectively. + RateLimitConfig *RedisConfig `env:", prefix=RATE_LIMIT_"` + } + ``` -The value for `Port` will default to the value of `DEFAULT_PORT`. +- `overwrite` - force overwriting existing non-zero struct values if the + environment variable was provided. -It is invalid to have a field as both `required` and `default`. + ```go + type MyStruct struct { + Port string `env:"PORT, overwrite"` + } + ``` -### Prefix + The rules for overwrite + default are: -For shared, embedded structs, you can define a prefix to use when processing -struct values for that embed. + - If the struct field has the zero value and a default is set: -```go -type SharedConfig struct { - Port int `env:"PORT,default=5555"` -} + - If no environment variable is specified, the struct field will be + populated with the default value. -type Server1 struct { - // This processes Port from $FOO_PORT. - *SharedConfig `env:",prefix=FOO_"` -} + - If an environment variable is specified, the struct field will be + populate with the environment variable value. -type Server2 struct { - // This processes Port from $BAR_PORT. - *SharedConfig `env:",prefix=BAR_"` -} -``` + - If the struct field has a non-zero value and a default is set: -It is invalid to specify a prefix on non-struct fields. + - If no environment variable is specified, the struct field's existing + value will be used (the default is ignored). -### Overwrite + - If an environment variable is specified, the struct field's existing + value will be overwritten with the environment variable value. -If overwrite is set, the value will be overwritten if there is an environment -variable match regardless if the value is non-zero. +- `delimiter` - choose a custom character to denote individual slice and map + entries. The default value is the comma (`,`). -```go -type MyStruct struct { - Port int `env:"PORT,overwrite"` -} -``` + ```go + type MyStruct struct { + MyVar []string `env:"MYVAR, delimiter=;"` + ``` + + ```bash + export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"} + ``` + +- `separator` - choose a custom character to denote the separation between + keys and values in map entries. The default value is the colon (`:`) Define + a separator with `separator`: -The rules for overwrite + default are: + ```go + type MyStruct struct { + MyVar map[string]string `env:"MYVAR, separator=|"` + } + ``` -- If the struct field has the zero value and a default is set: + ```bash + export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"} + ``` - - If no environment variable is specified, the struct field will be - populated with the default value. +- `noinit` - do not initialize struct fields unless environment variables were + provided. The default behavior is to deeply initialize all fields to their + default (zero) value. - - If an environment variable is specified, the struct field will be - populate with the environment variable value. + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, noinit"` + } + ``` -- If the struct field has a non-zero value and a default is set: +- `decodeunset` - force envconfig to run decoders even on unset environment + variable values. The default behavior is to skip running decoders on unset + environment variable values. - - If no environment variable is specified, the struct field's existing - value will be used (the default is ignored). + ```go + type MyStruct struct { + MyVar *url.URL `env:"MYVAR, decodeunset"` + } + ``` - - If an environment variable is specified, the struct field's existing - value will be overwritten with the environment variable value. +## Decoding -## Complex Types +> [!NOTE] +> +> Complex types are only decoded or unmarshalled when the environment variable +> is defined or a default value is specified. -**Note:** Complex types are only decoded or unmarshalled when the environment -variable is defined or a default is specified. The decoding/unmarshalling -functions are _not_ invoked when a value is not defined. ### Durations @@ -186,14 +210,18 @@ type MyStruct struct { export MYVAR="10m" # 10 * time.Minute ``` + ### TextUnmarshaler / BinaryUnmarshaler -Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as such. +Types that implement `TextUnmarshaler` or `BinaryUnmarshaler` are processed as +such. + ### json.Unmarshaler Types that implement `json.Unmarshaler` are processed as such. + ### gob.Decoder Types that implement `gob.Decoder` are processed as such. @@ -201,7 +229,7 @@ Types that implement `gob.Decoder` are processed as such. ### Slices -Slices are specified as comma-separated values: +Slices are specified as comma-separated values. ```go type MyStruct struct { @@ -213,20 +241,10 @@ type MyStruct struct { export MYVAR="a,b,c,d" # []string{"a", "b", "c", "d"} ``` -Define a custom delimiter with `delimiter`: - -```go -type MyStruct struct { - MyVar []string `env:"MYVAR,delimiter=;"` -``` - -```bash -export MYVAR="a;b;c;d" # []string{"a", "b", "c", "d"} -``` - Note that byte slices are special cased and interpreted as strings from the environment. + ### Maps Maps are specified as comma-separated key:value pairs: @@ -241,29 +259,6 @@ type MyStruct struct { export MYVAR="a:b,c:d" # map[string]string{"a":"b", "c":"d"} ``` -Define a custom delimiter with `delimiter`: - -```go -type MyStruct struct { - MyVar map[string]string `env:"MYVAR,delimiter=;"` -``` - -```bash -export MYVAR="a:b;c:d" # map[string]string{"a":"b", "c":"d"} -``` - -Define a separator with `separator`: - -```go -type MyStruct struct { - MyVar map[string]string `env:"MYVAR,separator=|"` -} -``` - -```bash -export MYVAR="a|b,c|d" # map[string]string{"a":"b", "c":"d"} -``` - ### Structs @@ -275,101 +270,11 @@ the non-nil value. To change this behavior, see [Initialization](#Initialization). -### Custom - -You can also [define your own decoder](#Extension). +### Custom Decoders +You can also define your own decoders. See the [godoc][godoc] for more +information. -## Prefixing - -You can define a custom prefix using the `PrefixLookuper`. This will lookup -values in the environment by prefixing the keys with the provided value: - -```go -type MyStruct struct { - MyVar string `env:"MYVAR"` -} -``` - -```go -// Process variables, but look for the "APP_" prefix. -l := envconfig.PrefixLookuper("APP_", envconfig.OsLookuper()) -if err := envconfig.ProcessWith(ctx, &c, l); err != nil { - panic(err) -} -``` - -```bash -export APP_MYVAR="foo" -``` - -## Initialization - -By default, all pointers, slices, and maps are initialized (allocated) so they -are not `nil`. To disable this behavior, use the tag the field as `noinit`: - -```go -type MyStruct struct { - // Without `noinit`, DeleteUser would be initialized to the default boolean - // value. With `noinit`, if the environment variable is not given, the value - // is kept as uninitialized (nil). - DeleteUser *bool `env:"DELETE_USER, noinit"` -} -``` - -This also applies to nested fields in a struct: - -```go -type ParentConfig struct { - // Without `noinit` tag, `Child` would be set to `&ChildConfig{}` whether - // or not `FIELD` is set in the env var. - // With `noinit`, `Child` would stay nil if `FIELD` is not set in the env var. - Child *ChildConfig `env:",noinit"` -} - -type ChildConfig struct { - Field string `env:"FIELD"` -} -``` - -The `noinit` tag is only applicable for pointer, slice, and map fields. Putting -the tag on a different type will return an error. - - -## Extension - -All built-in types are supported except `Func` and `Chan`. If you need to define -a custom decoder, implement the `Decoder` interface: - -```go -type MyStruct struct { - field string -} - -func (v *MyStruct) EnvDecode(val string) error { - v.field = fmt.Sprintf("PREFIX-%s", val) - return nil -} -``` - -If you need to modify environment variable values before processing, you can -specify a custom `Mutator`: - -```go -type Config struct { - Password `env:"PASSWORD"` -} - -func resolveSecretFunc(ctx context.Context, key, value string) (string, error) { - if strings.HasPrefix(value, "secret://") { - return secretmanager.Resolve(ctx, value) // example - } - return value, nil -} - -var config Config -envconfig.ProcessWith(ctx, &config, envconfig.OsLookuper(), resolveSecretFunc) -``` ## Testing @@ -384,7 +289,9 @@ lookuper := envconfig.MapLookuper(map[string]string{ }) var config Config -envconfig.ProcessWith(ctx, &config, lookuper) +envconfig.ProcessWith(ctx, &config, &envconfig.Config{ + Lookuper: lookuper, +}) ``` Now you can parallelize all your tests by providing a map for the lookup @@ -395,20 +302,4 @@ You can also combine multiple lookupers with `MultiLookuper`. See the GoDoc for more information and examples. -## Inspiration - -This library is conceptually similar to [kelseyhightower/envconfig](https://github.com/kelseyhightower/envconfig), with the following -major behavioral differences: - -- Adds support for specifying a custom lookup function (such as a map), which - is useful for testing. - -- Only populates fields if they contain zero or nil values if `overwrite` is - unset. This means you can pre-initialize a struct and any pre-populated - fields will not be overwritten during processing. - -- Support for interpolation. The default value for a field can be the value of - another field. - -- Support for arbitrary mutators that change/resolve data before type - conversion. +[godoc]: https://pkg.go.dev/mod/github.com/sethvargo/go-envconfig diff --git a/vendor/github.com/sethvargo/go-envconfig/envconfig.go b/vendor/github.com/sethvargo/go-envconfig/envconfig.go index 2d520592f30..6c85866b9e1 100644 --- a/vendor/github.com/sethvargo/go-envconfig/envconfig.go +++ b/vendor/github.com/sethvargo/go-envconfig/envconfig.go @@ -46,22 +46,7 @@ // // export MYVAR="a:b,c:d" // map[string]string{"a":"b", "c":"d"} // -// If you need to modify environment variable values before processing, you can -// specify a custom mutator: -// -// type Config struct { -// Password `env:"PASSWORD_SECRET"` -// } -// -// func resolveSecretFunc(ctx context.Context, key, value string) (string, error) { -// if strings.HasPrefix(value, "secret://") { -// return secretmanager.Resolve(ctx, value) // example -// } -// return value, nil -// } -// -// var config Config -// ProcessWith(&config, OsLookuper(), resolveSecretFunc) +// For more configuration options and examples, see the documentation. package envconfig import ( @@ -82,39 +67,37 @@ import ( const ( envTag = "env" - optDefault = "default=" - optDelimiter = "delimiter=" - optNoInit = "noinit" - optOverwrite = "overwrite" - optPrefix = "prefix=" - optRequired = "required" - optSeparator = "separator=" - - defaultDelimiter = "," - defaultSeparator = ":" + optDecodeUnset = "decodeunset" + optDefault = "default=" + optDelimiter = "delimiter=" + optNoInit = "noinit" + optOverwrite = "overwrite" + optPrefix = "prefix=" + optRequired = "required" + optSeparator = "separator=" ) -// Error is a custom error type for errors returned by envconfig. -type Error string +// internalError is a custom error type for errors returned by envconfig. +type internalError string // Error implements error. -func (e Error) Error() string { +func (e internalError) Error() string { return string(e) } const ( - ErrInvalidEnvvarName = Error("invalid environment variable name") - ErrInvalidMapItem = Error("invalid map item") - ErrLookuperNil = Error("lookuper cannot be nil") - ErrMissingKey = Error("missing key") - ErrMissingRequired = Error("missing required value") - ErrNoInitNotPtr = Error("field must be a pointer to have noinit") - ErrNotPtr = Error("input must be a pointer") - ErrNotStruct = Error("input must be a struct") - ErrPrefixNotStruct = Error("prefix is only valid on struct types") - ErrPrivateField = Error("cannot parse private fields") - ErrRequiredAndDefault = Error("field cannot be required and have a default value") - ErrUnknownOption = Error("unknown option") + ErrInvalidEnvvarName = internalError("invalid environment variable name") + ErrInvalidMapItem = internalError("invalid map item") + ErrLookuperNil = internalError("lookuper cannot be nil") + ErrMissingKey = internalError("missing key") + ErrMissingRequired = internalError("missing required value") + ErrNoInitNotPtr = internalError("field must be a pointer to have noinit") + ErrNotPtr = internalError("input must be a pointer") + ErrNotStruct = internalError("input must be a struct") + ErrPrefixNotStruct = internalError("prefix is only valid on struct types") + ErrPrivateField = internalError("cannot parse private fields") + ErrRequiredAndDefault = internalError("field cannot be required and have a default value") + ErrUnknownOption = internalError("unknown option") ) // Lookuper is an interface that provides a lookup for a string-based key. @@ -188,7 +171,24 @@ type prefixLookuper struct { } func (p *prefixLookuper) Lookup(key string) (string, bool) { - return p.l.Lookup(p.prefix + key) + return p.l.Lookup(p.Key(key)) +} + +func (p *prefixLookuper) Key(key string) string { + return p.prefix + key +} + +func (p *prefixLookuper) Unwrap() Lookuper { + l := p.l + for v, ok := l.(unwrappableLookuper); ok; { + l = v.Unwrap() + } + return l +} + +// unwrappableLookuper is a lookuper that can return the underlying lookuper. +type unwrappableLookuper interface { + Unwrap() Lookuper } // MultiLookuper wraps a collection of lookupers. It does not combine them, and @@ -197,6 +197,12 @@ func MultiLookuper(lookupers ...Lookuper) Lookuper { return &multiLookuper{ls: lookupers} } +// keyedLookuper is an extension to the [Lookuper] interface that returns the +// underlying key (used by the [PrefixLookuper] or custom implementations). +type keyedLookuper interface { + Key(key string) string +} + // Decoder is an interface that custom types/fields can implement to control how // decoding takes place. For example: // @@ -209,59 +215,95 @@ type Decoder interface { EnvDecode(val string) error } -// MutatorFunc is a function that mutates a given value before it is passed -// along for processing. This is useful if you want to mutate the environment -// variable value before it's converted to the proper type. -type MutatorFunc func(ctx context.Context, k, v string) (string, error) - // options are internal options for decoding. type options struct { - Default string - Delimiter string - Prefix string - Separator string - NoInit bool - Overwrite bool - Required bool + Default string + Delimiter string + Prefix string + Separator string + NoInit bool + Overwrite bool + DecodeUnset bool + Required bool } -// Process processes the struct using the environment. See [ProcessWith] for a -// more customizable version. -func Process(ctx context.Context, i interface{}) error { - return ProcessWith(ctx, i, OsLookuper()) +// Config represent inputs to the envconfig decoding. +type Config struct { + // Target is the destination structure to decode. This value is required, and + // it must be a pointer to a struct. + Target any + + // Lookuper is the lookuper implementation to use. If not provided, it + // defaults to the OS Lookuper. + Lookuper Lookuper + + // DefaultDelimiter is the default value to use for the delimiter in maps and + // slices. This can be overridden on a per-field basis, which takes + // precedence. The default value is ",". + DefaultDelimiter string + + // DefaultSeparator is the default value to use for the separator in maps. + // This can be overridden on a per-field basis, which takes precedence. The + // default value is ":". + DefaultSeparator string + + // DefaultNoInit is the default value for skipping initialization of + // unprovided fields. The default value is false (deeply initialize all + // fields and nested structs). + DefaultNoInit bool + + // DefaultOverwrite is the default value for overwriting an existing value set + // on the struct before processing. The default value is false. + DefaultOverwrite bool + + // DefaultDecodeUnset is the default value for running decoders even when no + // value was given for the environment variable. + DefaultDecodeUnset bool + + // DefaultRequired is the default value for marking a field as required. The + // default value is false. + DefaultRequired bool + + // Mutators is an optiona list of mutators to apply to lookups. + Mutators []Mutator } -// ProcessWith processes the given interface with the given lookuper. See the -// package-level documentation for specific examples and behaviors. -func ProcessWith(ctx context.Context, i interface{}, l Lookuper, fns ...MutatorFunc) error { - return processWith(ctx, i, l, false, fns...) +// Process decodes the struct using values from environment variables. See +// [ProcessWith] for a more customizable version. +func Process(ctx context.Context, i any, mus ...Mutator) error { + return ProcessWith(ctx, &Config{ + Target: i, + Mutators: mus, + }) } -// ExtractDefaults is a helper that returns a fully-populated struct with the -// default values resolved. This is helpful when you want to return a constant -// "default" configuration that is not affected by the user's environment. -// -// type Config struct { -// Port string `env:"PORT,default=8080"` -// } -// -// func DefaultConfig() *Config { -// var cfg Config -// if err := envconfig.ExtractDefaults(ctx, &cfg); err != nil { -// panic("failed to extract default config: %s" + err.Error()) -// } -// return &cfg -// } -// -// This is effectively the same as calling [ProcessWith] with an empty -// [MapLookuper]. -func ExtractDefaults(ctx context.Context, i interface{}, fns ...MutatorFunc) error { - return processWith(ctx, i, MapLookuper(nil), false, fns...) +// ProcessWith executes the decoding process using the provided [Config]. +func ProcessWith(ctx context.Context, c *Config) error { + if c == nil { + c = new(Config) + } + + if c.Lookuper == nil { + c.Lookuper = OsLookuper() + } + + // Deep copy the slice and remove any nil mutators. + var mus []Mutator + for _, m := range c.Mutators { + if m != nil { + mus = append(mus, m) + } + } + c.Mutators = mus + + return processWith(ctx, c) } -// processWith is a helper that captures whether the parent wanted -// initialization. -func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bool, fns ...MutatorFunc) error { +// processWith is a helper that retains configuration from the parent structs. +func processWith(ctx context.Context, c *Config) error { + i := c.Target + + l := c.Lookuper if l == nil { return ErrLookuperNil } @@ -278,6 +320,24 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo t := e.Type() + structDelimiter := c.DefaultDelimiter + if structDelimiter == "" { + structDelimiter = "," + } + + structNoInit := c.DefaultNoInit + + structSeparator := c.DefaultSeparator + if structSeparator == "" { + structSeparator = ":" + } + + structOverwrite := c.DefaultOverwrite + structDecodeUnset := c.DefaultDecodeUnset + structRequired := c.DefaultRequired + + mutators := c.Mutators + for i := 0; i < t.NumField(); i++ { ef := e.Field(i) tf := t.Field(i) @@ -308,7 +368,21 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo ef.Kind() != reflect.UnsafePointer { return fmt.Errorf("%s: %w", tf.Name, ErrNoInitNotPtr) } - shouldNotInit := opts.NoInit || parentNoInit + + // Compute defaults from local tags. + delimiter := structDelimiter + if v := opts.Delimiter; v != "" { + delimiter = v + } + separator := structSeparator + if v := opts.Separator; v != "" { + separator = v + } + + noInit := structNoInit || opts.NoInit + overwrite := structOverwrite || opts.Overwrite + decodeUnset := structDecodeUnset || opts.DecodeUnset + required := structRequired || opts.Required isNilStructPtr := false setNilStruct := func(v reflect.Value) { @@ -319,7 +393,7 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo // If a struct (after traversal) equals to the empty value, it means // nothing was changed in any sub-fields. With the noinit opt, we skip // setting the empty value to the original struct pointer (keep it nil). - if !reflect.DeepEqual(v.Interface(), empty) || !shouldNotInit { + if !reflect.DeepEqual(v.Interface(), empty) || !noInit { origin.Set(v) } } @@ -354,18 +428,20 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo // Lookup the value, ignoring an error if the key isn't defined. This is // required for nested structs that don't declare their own `env` keys, // but have internal fields with an `env` defined. - val, _, _, err := lookup(key, opts, l) + val, found, usedDefault, err := lookup(key, required, opts.Default, l) if err != nil && !errors.Is(err, ErrMissingKey) { return fmt.Errorf("%s: %w", tf.Name, err) } - if ok, err := processAsDecoder(val, ef); ok { - if err != nil { - return err - } + if found || usedDefault || decodeUnset { + if ok, err := processAsDecoder(val, ef); ok { + if err != nil { + return err + } - setNilStruct(ef) - continue + setNilStruct(ef) + continue + } } plu := l @@ -373,7 +449,16 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo plu = PrefixLookuper(opts.Prefix, l) } - if err := processWith(ctx, ef.Interface(), plu, shouldNotInit, fns...); err != nil { + if err := processWith(ctx, &Config{ + Target: ef.Interface(), + Lookuper: plu, + DefaultDelimiter: delimiter, + DefaultSeparator: separator, + DefaultNoInit: noInit, + DefaultOverwrite: overwrite, + DefaultRequired: required, + Mutators: mutators, + }); err != nil { return fmt.Errorf("%s: %w", tf.Name, err) } @@ -394,11 +479,11 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo // The field already has a non-zero value and overwrite is false, do not // overwrite. - if (pointerWasSet || !ef.IsZero()) && !opts.Overwrite { + if (pointerWasSet || !ef.IsZero()) && !overwrite { continue } - val, found, usedDefault, err := lookup(key, opts, l) + val, found, usedDefault, err := lookup(key, required, opts.Default, l) if err != nil { return fmt.Errorf("%s: %w", tf.Name, err) } @@ -414,28 +499,27 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo // type conversions. They always resolve to a string (or error), so we don't // call mutators when the environment variable was not set. if found || usedDefault { - for _, fn := range fns { - if fn != nil { - val, err = fn(ctx, key, val) - if err != nil { - return fmt.Errorf("%s: %w", tf.Name, err) - } - } + originalKey := key + resolvedKey := originalKey + if keyer, ok := l.(keyedLookuper); ok { + resolvedKey = keyer.Key(resolvedKey) } - } - - // If Delimiter is not defined set it to "," - if opts.Delimiter == "" { - opts.Delimiter = defaultDelimiter - } + originalValue := val + stop := false - // If Separator is not defined set it to ":" - if opts.Separator == "" { - opts.Separator = defaultSeparator + for _, mu := range mutators { + val, stop, err = mu.EnvMutate(ctx, originalKey, resolvedKey, originalValue, val) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + if stop { + break + } + } } // Set value. - if err := processField(val, ef, opts.Delimiter, opts.Separator, opts.NoInit); err != nil { + if err := processField(val, ef, delimiter, separator, noInit); err != nil { return fmt.Errorf("%s(%q): %w", tf.Name, val, err) } } @@ -443,10 +527,24 @@ func processWith(ctx context.Context, i interface{}, l Lookuper, parentNoInit bo return nil } +// SplitString splits the given string on the provided rune, unless the rune is +// escaped by the escape character. +func splitString(s string, on string, esc string) []string { + a := strings.Split(s, on) + + for i := len(a) - 2; i >= 0; i-- { + if strings.HasSuffix(a[i], esc) { + a[i] = a[i][:len(a[i])-len(esc)] + on + a[i+1] + a = append(a[:i+1], a[i+2:]...) + } + } + return a +} + // keyAndOpts parses the given tag value (e.g. env:"foo,required") and // returns the key name and options as a list. func keyAndOpts(tag string) (string, *options, error) { - parts := strings.Split(tag, ",") + parts := splitString(tag, ",", "\\") key, tagOpts := strings.TrimSpace(parts[0]), parts[1:] if key != "" && !validateEnvName(key) { @@ -458,20 +556,24 @@ func keyAndOpts(tag string) (string, *options, error) { LOOP: for i, o := range tagOpts { o = strings.TrimLeftFunc(o, unicode.IsSpace) + search := strings.ToLower(o) + switch { - case o == optOverwrite: + case search == optDecodeUnset: + opts.DecodeUnset = true + case search == optOverwrite: opts.Overwrite = true - case o == optRequired: + case search == optRequired: opts.Required = true - case o == optNoInit: + case search == optNoInit: opts.NoInit = true - case strings.HasPrefix(o, optPrefix): + case strings.HasPrefix(search, optPrefix): opts.Prefix = strings.TrimPrefix(o, optPrefix) - case strings.HasPrefix(o, optDelimiter): + case strings.HasPrefix(search, optDelimiter): opts.Delimiter = strings.TrimPrefix(o, optDelimiter) - case strings.HasPrefix(o, optSeparator): + case strings.HasPrefix(search, optSeparator): opts.Separator = strings.TrimPrefix(o, optSeparator) - case strings.HasPrefix(o, optDefault): + case strings.HasPrefix(search, optDefault): // If a default value was given, assume everything after is the provided // value, including comma-seprated items. o = strings.TrimLeft(strings.Join(tagOpts[i:], ","), " ") @@ -489,7 +591,7 @@ LOOP: // first boolean parameter indicates whether the value was found in the // lookuper. The second boolean parameter indicates whether the default value // was used. -func lookup(key string, opts *options, l Lookuper) (string, bool, bool, error) { +func lookup(key string, required bool, defaultValue string, l Lookuper) (string, bool, bool, error) { if key == "" { // The struct has something like `env:",required"`, which is likely a // mistake. We could try to infer the envvar from the field name, but that @@ -497,7 +599,7 @@ func lookup(key string, opts *options, l Lookuper) (string, bool, bool, error) { return "", false, false, ErrMissingKey } - if opts.Required && opts.Default != "" { + if required && defaultValue != "" { // Having a default value on a required value doesn't make sense. return "", false, false, ErrRequiredAndDefault } @@ -505,19 +607,24 @@ func lookup(key string, opts *options, l Lookuper) (string, bool, bool, error) { // Lookup value. val, found := l.Lookup(key) if !found { - if opts.Required { - if pl, ok := l.(*prefixLookuper); ok { - key = pl.prefix + key + if required { + if keyer, ok := l.(keyedLookuper); ok { + key = keyer.Key(key) } return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, key) } - if opts.Default != "" { + if defaultValue != "" { // Expand the default value. This allows for a default value that maps to - // a different variable. - val = os.Expand(opts.Default, func(i string) string { - s, ok := l.Lookup(i) + // a different environment variable. + val = os.Expand(defaultValue, func(i string) string { + lookuper := l + if v, ok := lookuper.(unwrappableLookuper); ok { + lookuper = v.Unwrap() + } + + s, ok := lookuper.Lookup(i) if ok { return s } diff --git a/vendor/github.com/sethvargo/go-envconfig/mutator.go b/vendor/github.com/sethvargo/go-envconfig/mutator.go new file mode 100644 index 00000000000..ccfadba11c3 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/mutator.go @@ -0,0 +1,76 @@ +// Copyright The envconfig Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package envconfig + +import "context" + +// Mutator is the interface for a mutator function. Mutators act like middleware +// and alter values for subsequent processing. This is useful if you want to +// mutate the environment variable value before it's converted to the proper +// type. +// +// Mutators are only called on defined values (or when decodeunset is true). +type Mutator interface { + // EnvMutate is called to alter the environment variable value. + // + // - `originalKey` is the unmodified environment variable name as it was defined + // on the struct. + // + // - `resolvedKey` is the fully-resolved environment variable name, which may + // include prefixes or modifications from processing. When there are + // no modifications, this will be equivalent to `originalKey`. + // + // - `originalValue` is the unmodified environment variable's value before any + // mutations were run. + // + // - `currentValue` is the currently-resolved value, which may have been + // modified by previous mutators and may be modified in the future by + // subsequent mutators in the stack. + // + // The function returns (in order): + // + // - The new value to use in both future mutations and final processing. + // + // - A boolean which indicates whether future mutations in the stack should be + // applied. + // + // - Any errors that occurred. + // + EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) +} + +var _ Mutator = (MutatorFunc)(nil) + +// MutatorFunc implements the [Mutator] and provides a quick way to create an +// anonymous function. +type MutatorFunc func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) + +// EnvMutate implements [Mutator]. +func (m MutatorFunc) EnvMutate(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { + return m(ctx, originalKey, resolvedKey, originalValue, currentValue) +} + +// LegacyMutatorFunc is a helper that eases the transition from the previous +// MutatorFunc signature. It wraps the previous-style mutator function and +// returns a new one. Since the former mutator function had less data, this is +// inherently lossy. +// +// Deprecated: Use [MutatorFunc] instead. +func LegacyMutatorFunc(fn func(ctx context.Context, key, value string) (string, error)) MutatorFunc { + return func(ctx context.Context, originalKey, resolvedKey, originalValue, currentValue string) (newValue string, stop bool, err error) { + v, err := fn(ctx, originalKey, currentValue) + return v, true, err + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 440182abe7e..298ca652306 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -801,8 +801,8 @@ github.com/segmentio/backo-go # github.com/sergi/go-diff v1.3.1 ## explicit; go 1.12 github.com/sergi/go-diff/diffmatchpatch -# github.com/sethvargo/go-envconfig v0.9.0 -## explicit; go 1.17 +# github.com/sethvargo/go-envconfig v1.0.0 +## explicit; go 1.21 github.com/sethvargo/go-envconfig # github.com/sirupsen/logrus v1.9.2 ## explicit; go 1.13