diff --git a/cmd/pact-proxy/main.go b/cmd/pact-proxy/main.go index 6387c9b..7f12cdd 100644 --- a/cmd/pact-proxy/main.go +++ b/cmd/pact-proxy/main.go @@ -4,21 +4,22 @@ import ( "os" "os/signal" "strconv" - "strings" "syscall" "github.com/form3tech-oss/pact-proxy/internal/app/configuration" + "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy" log "github.com/sirupsen/logrus" ) func main() { - proxies := os.Getenv("PROXIES") - for _, proxy := range strings.Split(strings.TrimSpace(proxies), ";") { - if proxy != "" { - log.Infof("setting up proxy for %s", proxy) - if err := configuration.ConfigureProxy(configuration.ProxyConfig{Target: proxy}); err != nil { - panic(err) - } + config, err := configuration.NewFromEnv() + if err != nil { + log.WithError(err).Fatal("unable to load configuration") + } + for _, proxy := range config.Proxies { + log.Infof("setting up proxy for %s", proxy) + if err := configuration.ConfigureProxy(pactproxy.Config{Target: proxy}); err != nil { + panic(err) } } diff --git a/go.mod b/go.mod index 8e632fd..ec16eaa 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/labstack/echo/v4 v4.9.0 github.com/pact-foundation/pact-go v1.7.0 github.com/pkg/errors v0.9.1 + github.com/sethvargo/go-envconfig v0.8.2 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.0 github.com/tidwall/sjson v1.2.4 diff --git a/go.sum b/go.sum index a36e82d..a1ab652 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -208,6 +209,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-envconfig v0.8.2 h1:DDUVuG21RMgeB/bn4leclUI/837y6cQCD4w8hb5797k= +github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= diff --git a/internal/app/configuration/api.go b/internal/app/configuration/api.go index 0d3f9b9..b7025e8 100644 --- a/internal/app/configuration/api.go +++ b/internal/app/configuration/api.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/form3tech-oss/pact-proxy/internal/app/httpresponse" + "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy" "github.com/labstack/echo/v4" log "github.com/sirupsen/logrus" ) @@ -34,7 +35,7 @@ func deleteProxiesHandler(c echo.Context) error { func postProxiesHandler(c echo.Context) error { - proxyConfig := ProxyConfig{} + proxyConfig := pactproxy.Config{} err := c.Bind(&proxyConfig) if err != nil { return c.JSON( diff --git a/internal/app/configuration/proxyconfig.go b/internal/app/configuration/proxyconfig.go index 7ce197e..0cf21b4 100644 --- a/internal/app/configuration/proxyconfig.go +++ b/internal/app/configuration/proxyconfig.go @@ -1,38 +1,30 @@ package configuration import ( - "fmt" - "net/url" + "context" "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy" + "github.com/pkg/errors" + "github.com/sethvargo/go-envconfig" ) -type ProxyConfig struct { - ServerAddress string - Target string -} +func NewFromEnv() (pactproxy.Config, error) { + ctx := context.Background() -func ConfigureProxy(config ProxyConfig) error { - targetURL, err := url.Parse(config.Target) + var config pactproxy.Config + err := envconfig.Process(ctx, &config) if err != nil { - return err - } - - serverAddr := config.ServerAddress - if serverAddr == "" { - serverAddr = fmt.Sprintf("http://:%s", targetURL.Port()) - } - - serverAddrURL, err := url.Parse(serverAddr) - if err != nil { - return err + return config, errors.Wrap(err, "process env config") } + return config, nil +} - server, err := GetServer(serverAddrURL) +func ConfigureProxy(config pactproxy.Config) error { + server, err := GetServer(&config.ServerAddress) if err != nil { return err } - pactproxy.StartProxy(server, targetURL) + pactproxy.StartProxy(server, &config) return err } diff --git a/internal/app/pactproxy/proxy.go b/internal/app/pactproxy/proxy.go index 0c3ddd6..4719a7b 100644 --- a/internal/app/pactproxy/proxy.go +++ b/internal/app/pactproxy/proxy.go @@ -21,6 +21,14 @@ const ( defaultDuration = 15 * time.Second ) +type Config struct { + ServerAddress url.URL `env:"SERVER_ADDRESS"` // Address to listen on + Proxies []url.URL `env:"PROXIES,delimiter=;"` // List of URL to serve pact-proxy on, e.g. http://localhost:8080;http://localhost:8081 + WaitDelay time.Duration `env:"WAIT_DELAY"` // Default Delay for WaitForInteractions endpoint + WaitDuration time.Duration `env:"WAIT_DURATION"` // Default Duration for WaitForInteractions endpoint + Target url.URL // Do not load Target from env, we set this for each value from Proxies +} + var supportedMediaTypes = map[string]func([]byte, *url.URL) (requestDocument, error){ mediaTypeJSON: ParseJSONRequest, mediaTypeText: ParsePlainTextRequest, @@ -41,16 +49,22 @@ func (a *api) ProxyRequest(c echo.Context) error { return nil } -func StartProxy(e *echo.Echo, target *url.URL) { +func StartProxy(e *echo.Echo, config *Config) { // Create these once at startup, thay are shared by all server threads a := api{ - target: target, - proxy: httputil.NewSingleHostReverseProxy(target), + target: &config.Target, + proxy: httputil.NewSingleHostReverseProxy(&config.Target), interactions: &Interactions{}, notify: NewNotify(), - delay: defaultDelay, - duration: defaultDuration, + delay: config.WaitDelay, + duration: config.WaitDuration, + } + if a.delay == 0 { + a.delay = defaultDelay + } + if a.duration == 0 { + a.duration = defaultDuration } e.GET("/ready", a.readinessHandler) @@ -168,8 +182,13 @@ func (a *api) interactionsWaitHandler(c echo.Context) error { return c.JSON(http.StatusBadRequest, httpresponse.Errorf("cannot wait for interaction '%s', interaction not found.", waitFor)) } - log.Infof("waiting for %s", waitFor) + log.WithField("wait_for", waitFor).Infof("waiting") retryFor(func(timeLeft time.Duration) bool { + log.WithFields(log.Fields{ + "wait_for": waitFor, + "count": waitForCount, + "time_remaining": timeLeft, + }).Infof("retry") if interaction.HasRequests(waitForCount) { return true } diff --git a/pkg/pactproxy/proxyconfig.go b/pkg/pactproxy/proxyconfig.go index 6ddd236..0c1a98b 100644 --- a/pkg/pactproxy/proxyconfig.go +++ b/pkg/pactproxy/proxyconfig.go @@ -5,10 +5,11 @@ import ( "encoding/json" "io/ioutil" "net/http" + "net/url" "strings" "time" - "github.com/form3tech-oss/pact-proxy/internal/app/configuration" + "github.com/form3tech-oss/pact-proxy/internal/app/pactproxy" "github.com/pkg/errors" ) @@ -27,12 +28,24 @@ func Configuration(url string) *ProxyConfiguration { } func (conf *ProxyConfiguration) SetupProxy(serverAddress, targetAddress string) (*PactProxy, error) { - config := &configuration.ProxyConfig{ - ServerAddress: serverAddress, - Target: targetAddress, + serverURL, err := url.Parse(serverAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to parse server address") + } + targetURL, err := url.Parse(targetAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to parse target address") + } + + config := &pactproxy.Config{ + ServerAddress: *serverURL, + Target: *targetURL, } content, err := json.Marshal(config) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal config") + } req, err := http.NewRequest("POST", strings.TrimSuffix(conf.url, "/")+"/proxies", bytes.NewReader(content)) if err != nil { diff --git a/vendor/github.com/sethvargo/go-envconfig/AUTHORS b/vendor/github.com/sethvargo/go-envconfig/AUTHORS new file mode 100644 index 0000000..2107f4a --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/AUTHORS @@ -0,0 +1,8 @@ +# This is the list of envconfig authors for copyright purposes. +# +# This does not necessarily list everyone who has contributed code, since in +# some cases, their employer may be the copyright holder. To see the full list +# of contributors, see the revision history in source control. + +Google LLC +Seth Vargo diff --git a/vendor/github.com/sethvargo/go-envconfig/LICENSE b/vendor/github.com/sethvargo/go-envconfig/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/sethvargo/go-envconfig/Makefile b/vendor/github.com/sethvargo/go-envconfig/Makefile new file mode 100644 index 0000000..b95f7a7 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/Makefile @@ -0,0 +1,22 @@ +# 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. + +test: + @go test \ + -count=1 \ + -race \ + -shuffle=on \ + -timeout=10m \ + ./... +.PHONY: test diff --git a/vendor/github.com/sethvargo/go-envconfig/README.md b/vendor/github.com/sethvargo/go-envconfig/README.md new file mode 100644 index 0000000..3943cb1 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/README.md @@ -0,0 +1,414 @@ +# 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) + +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"` + Username string `env:"USERNAME"` +} +``` + +Set some environment variables: + +```sh +export PORT=5555 +export USERNAME=yoyo +``` + +Process it using envconfig: + +```go +package main + +import ( + "context" + "log" + + "github.com/sethvargo/go-envconfig" +) + +func main() { + ctx := context.Background() + + var c MyConfig + if err := envconfig.Process(ctx, &c); err != nil { + log.Fatal(err) + } + + // c.Port = 5555 + // c.Username = "yoyo" +} +``` + +You can also use nested structs, just remember that any fields you want to +process must be public: + +```go +type MyConfig struct { + Database *DatabaseConfig +} + +type DatabaseConfig struct { + Port int `env:"PORT"` + Username string `env:"USERNAME"` +} +``` + +## Configuration + +Use the `env` struct tag to define configuration. + +### Required + +If a field is required, processing will error if the environment variable is +unset. + +```go +type MyStruct struct { + Port int `env:"PORT,required"` +} +``` + +It is invalid to have a field as both `required` and `default`. + +### Default + +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. + +```go +type MyStruct struct { + Port int `env:"PORT,default=5555"` +} +``` + +You can also set the default value to another field or value from the +environment, for example: + +```go +type MyStruct struct { + DefaultPort int `env:"DEFAULT_PORT,default=5555"` + Port int `env:"OVERRIDE_PORT,default=$DEFAULT_PORT"` +} +``` + +The value for `Port` will default to the value of `DEFAULT_PORT`. + +It is invalid to have a field as both `required` and `default`. + +### Prefix + +For shared, embedded structs, you can define a prefix to use when processing +struct values for that embed. + +```go +type SharedConfig struct { + Port int `env:"PORT,default=5555"` +} + +type Server1 struct { + // This processes Port from $FOO_PORT. + *SharedConfig `env:",prefix=FOO_"` +} + +type Server2 struct { + // This processes Port from $BAR_PORT. + *SharedConfig `env:",prefix=BAR_"` +} +``` + +It is invalid to specify a prefix on non-struct fields. + +### Overwrite + +If overwrite is set, the value will be overwritten if there is an environment +variable match regardless if the value is non-zero. + +```go +type MyStruct struct { + Port int `env:"PORT,overwrite"` +} +``` + +The rules for overwrite + default are: + +- If the struct field has the zero value and a default is set: + + - If no environment variable is specified, the struct field will be + populated with the default value. + + - If an environment variable is specified, the struct field will be + populate with the environment variable value. + +- If the struct field has a non-zero value and a default is set: + + - If no environment variable is specified, the struct field's existing + value will be used (the default is ignored). + + - If an environment variable is specified, the struct field's existing + value will be overwritten with the environment variable value. + + +## Complex Types + +**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 + +In the environment, `time.Duration` values are specified as a parsable Go +duration: + +```go +type MyStruct struct { + MyVar time.Duration `env:"MYVAR"` +} +``` + +```bash +export MYVAR="10m" # 10 * time.Minute +``` + +### TextUnmarshaler / BinaryUnmarshaler + +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. + + +### Slices + +Slices are specified as comma-separated values: + +```go +type MyStruct struct { + MyVar []string `env:"MYVAR"` +} +``` + +```bash +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: + +```go +type MyStruct struct { + MyVar map[string]string `env:"MYVAR"` +} +``` + +```bash +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 + +Envconfig walks the entire struct, including nested structs, so deeply-nested +fields are also supported. + +If a nested struct is a pointer type, it will automatically be instantianted to +the non-nil value. To change this behavior, see +[Initialization](#Initialization). + + +### Custom + +You can also [define your own decoder](#Extension). + + +## 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 pointer fields 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 fields. Putting the tag on a +non-struct-pointer 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 + +Relying on the environment in tests can be troublesome because environment +variables are global, which makes it difficult to parallelize the tests. +Envconfig supports extracting data from anything that returns a value: + +```go +lookuper := envconfig.MapLookuper(map[string]string{ + "FOO": "bar", + "ZIP": "zap", +}) + +var config Config +envconfig.ProcessWith(ctx, &config, lookuper) +``` + +Now you can parallelize all your tests by providing a map for the lookup +function. In fact, that's how the tests in this repo work, so check there for an +example. + +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. diff --git a/vendor/github.com/sethvargo/go-envconfig/decoding.go b/vendor/github.com/sethvargo/go-envconfig/decoding.go new file mode 100644 index 0000000..3d1cd28 --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/decoding.go @@ -0,0 +1,57 @@ +// 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 ( + "encoding/base64" + "encoding/hex" + "strings" +) + +// Base64Bytes is a slice of bytes where the information is base64-encoded in +// the environment variable. +type Base64Bytes []byte + +// EnvDecode implements env.Decoder. +func (b *Base64Bytes) EnvDecode(val string) error { + val = strings.ReplaceAll(val, "+", "-") + val = strings.ReplaceAll(val, "/", "_") + val = strings.TrimRight(val, "=") + + var err error + *b, err = base64.RawURLEncoding.DecodeString(val) + return err +} + +// Bytes returns the underlying bytes. +func (b Base64Bytes) Bytes() []byte { + return []byte(b) +} + +// HexBytes is a slice of bytes where the information is hex-encoded in the +// environment variable. +type HexBytes []byte + +// EnvDecode implements env.Decoder. +func (b *HexBytes) EnvDecode(val string) error { + var err error + *b, err = hex.DecodeString(val) + return err +} + +// Bytes returns the underlying bytes. +func (b HexBytes) Bytes() []byte { + return []byte(b) +} diff --git a/vendor/github.com/sethvargo/go-envconfig/envconfig.go b/vendor/github.com/sethvargo/go-envconfig/envconfig.go new file mode 100644 index 0000000..6d55dfc --- /dev/null +++ b/vendor/github.com/sethvargo/go-envconfig/envconfig.go @@ -0,0 +1,717 @@ +// 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 populates struct fields based on environment variable +// values (or anything that responds to "Lookup"). Structs declare their +// environment dependencies using the "env" tag with the key being the name of +// the environment variable, case sensitive. +// +// type MyStruct struct { +// A string `env:"A"` // resolves A to $A +// B string `env:"B,required"` // resolves B to $B, errors if $B is unset +// C string `env:"C,default=foo"` // resolves C to $C, defaults to "foo" +// +// D string `env:"D,required,default=foo"` // error, cannot be required and default +// E string `env:""` // error, must specify key +// } +// +// All built-in types are supported except Func and Chan. If you need to define +// a custom decoder, implement Decoder: +// +// type MyStruct struct { +// field string +// } +// +// func (v *MyStruct) EnvDecode(val string) error { +// v.field = fmt.Sprintf("PREFIX-%s", val) +// return nil +// } +// +// In the environment, slices are specified as comma-separated values: +// +// export MYVAR="a,b,c,d" // []string{"a", "b", "c", "d"} +// +// In the environment, maps are specified as comma-separated key:value pairs: +// +// 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) +package envconfig + +import ( + "context" + "encoding" + "encoding/gob" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +const ( + envTag = "env" + + optDefault = "default=" + optDelimiter = "delimiter=" + optNoInit = "noinit" + optOverwrite = "overwrite" + optPrefix = "prefix=" + optRequired = "required" + optSeparator = "separator=" + + defaultDelimiter = "," + defaultSeparator = ":" +) + +// Error is a custom error type for errors returned by envconfig. +type Error string + +// Error implements error. +func (e Error) 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") +) + +// Lookuper is an interface that provides a lookup for a string-based key. +type Lookuper interface { + // Lookup searches for the given key and returns the corresponding string + // value. If a value is found, it returns the value and true. If a value is + // not found, it returns the empty string and false. + Lookup(key string) (string, bool) +} + +// osLookuper looks up environment configuration from the local environment. +type osLookuper struct{} + +// Verify implements interface. +var _ Lookuper = (*osLookuper)(nil) + +func (o *osLookuper) Lookup(key string) (string, bool) { + return os.LookupEnv(key) +} + +// OsLookuper returns a lookuper that uses the environment ([os.LookupEnv]) to +// resolve values. +func OsLookuper() Lookuper { + return new(osLookuper) +} + +type mapLookuper map[string]string + +var _ Lookuper = (*mapLookuper)(nil) + +func (m mapLookuper) Lookup(key string) (string, bool) { + v, ok := m[key] + return v, ok +} + +// MapLookuper looks up environment configuration from a provided map. This is +// useful for testing, especially in parallel, since it does not require you to +// mutate the parent environment (which is stateful). +func MapLookuper(m map[string]string) Lookuper { + return mapLookuper(m) +} + +type multiLookuper struct { + ls []Lookuper +} + +var _ Lookuper = (*multiLookuper)(nil) + +func (m *multiLookuper) Lookup(key string) (string, bool) { + for _, l := range m.ls { + if v, ok := l.Lookup(key); ok { + return v, true + } + } + return "", false +} + +// PrefixLookuper looks up environment configuration using the specified prefix. +// This is useful if you want all your variables to start with a particular +// prefix like "MY_APP_". +func PrefixLookuper(prefix string, l Lookuper) Lookuper { + if typ, ok := l.(*prefixLookuper); ok { + return &prefixLookuper{prefix: typ.prefix + prefix, l: typ.l} + } + return &prefixLookuper{prefix: prefix, l: l} +} + +type prefixLookuper struct { + l Lookuper + prefix string +} + +func (p *prefixLookuper) Lookup(key string) (string, bool) { + return p.l.Lookup(p.prefix + key) +} + +// MultiLookuper wraps a collection of lookupers. It does not combine them, and +// lookups appear in the order in which they are provided to the initializer. +func MultiLookuper(lookupers ...Lookuper) Lookuper { + return &multiLookuper{ls: lookupers} +} + +// Decoder is an interface that custom types/fields can implement to control how +// decoding takes place. For example: +// +// type MyType string +// +// func (mt MyType) EnvDecode(val string) error { +// return "CUSTOM-"+val +// } +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 +} + +// 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()) +} + +// 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...) +} + +// 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 { + if l == nil { + return ErrLookuperNil + } + + v := reflect.ValueOf(i) + if v.Kind() != reflect.Ptr { + return ErrNotPtr + } + + e := v.Elem() + if e.Kind() != reflect.Struct { + return ErrNotStruct + } + + t := e.Type() + + for i := 0; i < t.NumField(); i++ { + ef := e.Field(i) + tf := t.Field(i) + tag := tf.Tag.Get(envTag) + + if !ef.CanSet() { + if tag != "" { + // There's an "env" tag on a private field, we can't alter it, and it's + // likely a mistake. Return an error so the user can handle. + return fmt.Errorf("%s: %w", tf.Name, ErrPrivateField) + } + + // Otherwise continue to the next field. + continue + } + + // Parse the key and options. + key, opts, err := keyAndOpts(tag) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + // NoInit is only permitted on pointers. + if opts.NoInit && ef.Kind() != reflect.Ptr { + return fmt.Errorf("%s: %w", tf.Name, ErrNoInitNotPtr) + } + shouldNotInit := opts.NoInit || parentNoInit + + isNilStructPtr := false + setNilStruct := func(v reflect.Value) { + origin := e.Field(i) + if isNilStructPtr { + empty := reflect.New(origin.Type().Elem()).Interface() + + // 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 { + origin.Set(v) + } + } + } + + // Initialize pointer structs. + pointerWasSet := false + for ef.Kind() == reflect.Ptr { + if ef.IsNil() { + if ef.Type().Elem().Kind() != reflect.Struct { + // This is a nil pointer to something that isn't a struct, like + // *string. Move along. + break + } + + isNilStructPtr = true + // Use an empty struct of the type so we can traverse. + ef = reflect.New(ef.Type().Elem()).Elem() + } else { + pointerWasSet = true + ef = ef.Elem() + } + } + + // Special case handle structs. This has to come after the value resolution in + // case the struct has a custom decoder. + if ef.Kind() == reflect.Struct { + for ef.CanAddr() { + ef = ef.Addr() + } + + // 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) + 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 + } + + setNilStruct(ef) + continue + } + + plu := l + if opts.Prefix != "" { + plu = PrefixLookuper(opts.Prefix, l) + } + + if err := processWith(ctx, ef.Interface(), plu, shouldNotInit, fns...); err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + setNilStruct(ef) + continue + } + + // It's invalid to have a prefix on a non-struct field. + if opts.Prefix != "" { + return ErrPrefixNotStruct + } + + // Stop processing if there's no env tag (this comes after nested parsing), + // in case there's an env tag in an embedded struct. + if tag == "" { + continue + } + + // The field already has a non-zero value and overwrite is false, do not + // overwrite. + if (pointerWasSet || !ef.IsZero()) && !opts.Overwrite { + continue + } + + val, found, usedDefault, err := lookup(key, opts, l) + if err != nil { + return fmt.Errorf("%s: %w", tf.Name, err) + } + + // If the field already has a non-zero value and there was no value directly + // specified, do not overwrite the existing field. We only want to overwrite + // when the envvar was provided directly. + if (pointerWasSet || !ef.IsZero()) && !found { + continue + } + + // Apply any mutators. Mutators are applied after the lookup, but before any + // 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) + } + } + } + } + + // If Delimiter is not defined set it to "," + if opts.Delimiter == "" { + opts.Delimiter = defaultDelimiter + } + + // If Separator is not defined set it to ":" + if opts.Separator == "" { + opts.Separator = defaultSeparator + } + + // Set value. + if err := processField(val, ef, opts.Delimiter, opts.Separator, opts.NoInit); err != nil { + return fmt.Errorf("%s(%q): %w", tf.Name, val, err) + } + } + + return nil +} + +// 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, ",") + key, tagOpts := strings.TrimSpace(parts[0]), parts[1:] + + if key != "" && !validateEnvName(key) { + return "", nil, fmt.Errorf("%q: %w ", key, ErrInvalidEnvvarName) + } + + var opts options + +LOOP: + for i, o := range tagOpts { + o = strings.TrimSpace(o) + switch { + case o == optOverwrite: + opts.Overwrite = true + case o == optRequired: + opts.Required = true + case o == optNoInit: + opts.NoInit = true + case strings.HasPrefix(o, optPrefix): + opts.Prefix = strings.TrimPrefix(o, optPrefix) + case strings.HasPrefix(o, optDelimiter): + opts.Delimiter = strings.TrimPrefix(o, optDelimiter) + case strings.HasPrefix(o, optSeparator): + opts.Separator = strings.TrimPrefix(o, optSeparator) + case strings.HasPrefix(o, 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:], ","), " ") + opts.Default = strings.TrimPrefix(o, optDefault) + break LOOP + default: + return "", nil, fmt.Errorf("%q: %w", o, ErrUnknownOption) + } + } + + return key, &opts, nil +} + +// lookup looks up the given key using the provided Lookuper and options. The +// 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) { + 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 + // feels too magical. + return "", false, false, ErrMissingKey + } + + if opts.Required && opts.Default != "" { + // Having a default value on a required value doesn't make sense. + return "", false, false, ErrRequiredAndDefault + } + + // Lookup value. + val, found := l.Lookup(key) + if !found { + if opts.Required { + if pl, ok := l.(*prefixLookuper); ok { + key = pl.prefix + key + } + + return "", false, false, fmt.Errorf("%w: %s", ErrMissingRequired, key) + } + + if opts.Default != "" { + // 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) + if ok { + return s + } + return "" + }) + + return val, false, true, nil + } + } + + return val, found, false, nil +} + +// processAsDecoder processes the given value as a decoder or custom +// unmarshaller. +func processAsDecoder(v string, ef reflect.Value) (bool, error) { + // Keep a running error. It's possible that a property might implement + // multiple decoders, and we don't know *which* decoder will succeed. If we + // get through all of them, we'll return the most recent error. + var imp bool + var err error + + // Resolve any pointers. + for ef.CanAddr() { + ef = ef.Addr() + } + + if ef.CanInterface() { + iface := ef.Interface() + + // If a developer chooses to implement the Decoder interface on a type, + // never attempt to use other decoders in case of failure. EnvDecode's + // decoding logic is "the right one", and the error returned (if any) + // is the most specific we can get. + if dec, ok := iface.(Decoder); ok { + imp = true + err = dec.EnvDecode(v) + return imp, err + } + + if tu, ok := iface.(encoding.TextUnmarshaler); ok { + imp = true + if err = tu.UnmarshalText([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(json.Unmarshaler); ok { + imp = true + if err = tu.UnmarshalJSON([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(encoding.BinaryUnmarshaler); ok { + imp = true + if err = tu.UnmarshalBinary([]byte(v)); err == nil { + return imp, nil + } + } + + if tu, ok := iface.(gob.GobDecoder); ok { + imp = true + if err = tu.GobDecode([]byte(v)); err == nil { + return imp, nil + } + } + } + + return imp, err +} + +func processField(v string, ef reflect.Value, delimiter, separator string, noInit bool) error { + // If the input value is empty and initialization is skipped, do nothing. + if v == "" && noInit { + return nil + } + + // Handle pointers and uninitialized pointers. + for ef.Type().Kind() == reflect.Ptr { + if ef.IsNil() { + ef.Set(reflect.New(ef.Type().Elem())) + } + ef = ef.Elem() + } + + tf := ef.Type() + tk := tf.Kind() + + // Handle existing decoders. + if ok, err := processAsDecoder(v, ef); ok { + return err + } + + // We don't check if the value is empty earlier, because the user might want + // to define a custom decoder and treat the empty variable as a special case. + // However, if we got this far, none of the remaining parsers will succeed, so + // bail out now. + if v == "" { + return nil + } + + switch tk { + case reflect.Bool: + b, err := strconv.ParseBool(v) + if err != nil { + return err + } + ef.SetBool(b) + case reflect.Float32, reflect.Float64: + f, err := strconv.ParseFloat(v, tf.Bits()) + if err != nil { + return err + } + ef.SetFloat(f) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: + i, err := strconv.ParseInt(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetInt(i) + case reflect.Int64: + // Special case time.Duration values. + if tf.PkgPath() == "time" && tf.Name() == "Duration" { + d, err := time.ParseDuration(v) + if err != nil { + return err + } + ef.SetInt(int64(d)) + } else { + i, err := strconv.ParseInt(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetInt(i) + } + case reflect.String: + ef.SetString(v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + i, err := strconv.ParseUint(v, 0, tf.Bits()) + if err != nil { + return err + } + ef.SetUint(i) + + case reflect.Interface: + return fmt.Errorf("cannot decode into interfaces") + + // Maps + case reflect.Map: + vals := strings.Split(v, delimiter) + mp := reflect.MakeMapWithSize(tf, len(vals)) + for _, val := range vals { + pair := strings.SplitN(val, separator, 2) + if len(pair) < 2 { + return fmt.Errorf("%s: %w", val, ErrInvalidMapItem) + } + mKey, mVal := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1]) + + k := reflect.New(tf.Key()).Elem() + if err := processField(mKey, k, delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", mKey, err) + } + + v := reflect.New(tf.Elem()).Elem() + if err := processField(mVal, v, delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", mVal, err) + } + + mp.SetMapIndex(k, v) + } + ef.Set(mp) + + // Slices + case reflect.Slice: + // Special case: []byte + if tf.Elem().Kind() == reflect.Uint8 { + ef.Set(reflect.ValueOf([]byte(v))) + } else { + vals := strings.Split(v, delimiter) + s := reflect.MakeSlice(tf, len(vals), len(vals)) + for i, val := range vals { + val = strings.TrimSpace(val) + if err := processField(val, s.Index(i), delimiter, separator, noInit); err != nil { + return fmt.Errorf("%s: %w", val, err) + } + } + ef.Set(s) + } + } + + return nil +} + +// validateEnvName validates the given string conforms to being a valid +// environment variable. +// +// Per IEEE Std 1003.1-2001 environment variables consist solely of uppercase +// letters, digits, and _, and do not begin with a digit. +func validateEnvName(s string) bool { + if s == "" { + return false + } + + for i, r := range s { + if (i == 0 && !isLetter(r)) || (!isLetter(r) && !isNumber(r) && r != '_') { + return false + } + } + + return true +} + +// isLetter returns true if the given rune is a letter between a-z,A-Z. This is +// different than unicode.IsLetter which includes all L character cases. +func isLetter(r rune) bool { + return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') +} + +// isNumber returns true if the given run is a number between 0-9. This is +// different than unicode.IsNumber in that it only allows 0-9. +func isNumber(r rune) bool { + return r >= '0' && r <= '9' +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3f806ab..1896256 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -43,6 +43,9 @@ github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib +# github.com/sethvargo/go-envconfig v0.8.2 +## explicit; go 1.17 +github.com/sethvargo/go-envconfig # github.com/sirupsen/logrus v1.9.0 ## explicit; go 1.13 github.com/sirupsen/logrus