From bd0852bc25deb2f4aae3924cb4aab775b23f112b Mon Sep 17 00:00:00 2001 From: Ravi <1299606+rustycl0ck@users.noreply.github.com> Date: Sun, 3 Oct 2021 09:30:53 +0000 Subject: [PATCH] Add support for HTTP `POST` body content (#123) * Add support for HTTP POST body content * Add tests for POST body content * Code structure refactor for review --- README.md | 32 +++++++++ cmd/main.go | 2 +- cmd/main_test.go | 154 ++++++++++++++++++++++++++++++++++++++++++++ config/config.go | 6 ++ examples/config.yml | 11 ++++ exporter/util.go | 42 ++++++++++-- go.mod | 1 + go.sum | 20 ++++++ 8 files changed, 263 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 31cdb1b..adbbdf2 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,38 @@ TLS configuration supported by this exporter can be found at [exporter-toolkit/w make build ``` +## Sending body content for HTTP `POST` + +If `body` paramater is set in config, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. +```yaml +body: + content: | + My static information: {"time_diff": "1m25s", "anotherVar": "some value"} +``` + +The body content can also be a [Go Template](https://golang.org/pkg/text/template). All the functions from the [Sprig library](https://masterminds.github.io/sprig/) can be used in the template. +All the query parameters sent by prometheus in the scrape query to the exporter, are available as values while rendering the template. + +Example using template functions: +```yaml +body: + content: | + {"time_diff": "{{ duration `95` }}","anotherVar": "{{ randInt 12 30 }}"} + templatize: true +``` + +Example using template functions with values from the query parameters: +```yaml +body: + content: | + {"time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}"} + templatize: true +``` +Then `curl "http://exporter:7979/probe?target=http://scrape_target:8080/test/data.json&myVal=something"`, would result in sending the following body as the HTTP POST payload to `http://scrape_target:8080/test/data.json`: +``` +{"time_diff": "1m35s","anotherVar": "something"}. +``` + ## Docker ```console diff --git a/cmd/main.go b/cmd/main.go index 04ef4b2..e721001 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -102,7 +102,7 @@ func probeHandler(w http.ResponseWriter, r *http.Request, logger log.Logger, con return } - data, err := exporter.FetchJson(ctx, logger, target, config) + data, err := exporter.FetchJson(ctx, logger, target, config, r.URL.Query()) if err != nil { http.Error(w, "Failed to fetch JSON response. TARGET: "+target+", ERROR: "+err.Error(), http.StatusServiceUnavailable) return diff --git a/cmd/main_test.go b/cmd/main_test.go index 70df1de..b3fb52b 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -15,9 +15,11 @@ package cmd import ( "encoding/base64" + "io" "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" "github.com/go-kit/kit/log" @@ -218,3 +220,155 @@ func TestHTTPHeaders(t *testing.T) { t.Fatalf("Setting custom headers failed unexpectedly. Got: %s", body) } } + +// Test is the body template is correctly rendered +func TestBodyPostTemplate(t *testing.T) { + bodyTests := []struct { + Body config.ConfigBody + ShouldSucceed bool + Result string + }{ + { + Body: config.ConfigBody{Content: "something static like pi, 3.14"}, + ShouldSucceed: true, + }, + { + Body: config.ConfigBody{Content: "arbitrary dynamic value pass: {{ randInt 12 30 }}", Templatize: false}, + ShouldSucceed: true, + }, + { + Body: config.ConfigBody{Content: "arbitrary dynamic value fail: {{ randInt 12 30 }}", Templatize: true}, + ShouldSucceed: false, + }, + { + Body: config.ConfigBody{Content: "templatized mutated value: {{ upper `hello` }} is now all caps", Templatize: true}, + Result: "templatized mutated value: HELLO is now all caps", + ShouldSucceed: true, + }, + { + Body: config.ConfigBody{Content: "value should be {{ lower `All Small` | trunc 3 }}", Templatize: true}, + Result: "value should be all", + ShouldSucceed: true, + }, + } + + for _, test := range bodyTests { + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := test.Body.Content + if test.Result != "" { + expected = test.Result + } + if got, _ := io.ReadAll(r.Body); string(got) != expected && test.ShouldSucceed { + t.Errorf("POST request body content mismatch, got: %s, expected: %s", got, expected) + } + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest("POST", "http://example.com/foo"+"?target="+target.URL, strings.NewReader(test.Body.Content)) + recorder := httptest.NewRecorder() + c := config.Config{Body: test.Body} + + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + respBody, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("POST body content failed. Got: %s", respBody) + } + target.Close() + } +} + +// Test is the query parameters are correctly replaced in the provided body template +func TestBodyPostQuery(t *testing.T) { + bodyTests := []struct { + Body config.ConfigBody + ShouldSucceed bool + Result string + QueryParams map[string]string + }{ + { + Body: config.ConfigBody{Content: "pi has {{ .piValue | first }} value", Templatize: true}, + ShouldSucceed: true, + Result: "pi has 3.14 value", + QueryParams: map[string]string{"piValue": "3.14"}, + }, + { + Body: config.ConfigBody{Content: `{ "pi": "{{ .piValue | first }}" }`, Templatize: true}, + ShouldSucceed: true, + Result: `{ "pi": "3.14" }`, + QueryParams: map[string]string{"piValue": "3.14"}, + }, + { + Body: config.ConfigBody{Content: "pi has {{ .anotherQuery | first }} value", Templatize: true}, + ShouldSucceed: true, + Result: "pi has very high value", + QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "very high"}, + }, + { + Body: config.ConfigBody{Content: "pi has {{ .piValue }} value", Templatize: true}, + ShouldSucceed: false, + QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, + }, + { + Body: config.ConfigBody{Content: "pi has {{ .piValue }} value", Templatize: true}, + ShouldSucceed: true, + Result: "pi has [3.14] value", + QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, + }, + { + Body: config.ConfigBody{Content: "value of {{ upper `pi` | repeat 3 }} is {{ .anotherQuery | first }}", Templatize: true}, + ShouldSucceed: true, + Result: "value of PIPIPI is dummy value", + QueryParams: map[string]string{"piValue": "3.14", "anotherQuery": "dummy value"}, + }, + { + Body: config.ConfigBody{Content: "pi has {{ .piValue }} value", Templatize: true}, + ShouldSucceed: true, + Result: "pi has [] value", + }, + { + Body: config.ConfigBody{Content: "pi has {{ .piValue | first }} value", Templatize: true}, + ShouldSucceed: true, + Result: "pi has value", + }, + { + Body: config.ConfigBody{Content: "value of pi is 3.14", Templatize: true}, + ShouldSucceed: true, + }, + } + + for _, test := range bodyTests { + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := test.Body.Content + if test.Result != "" { + expected = test.Result + } + if got, _ := io.ReadAll(r.Body); string(got) != expected && test.ShouldSucceed { + t.Errorf("POST request body content mismatch (with query params), got: %s, expected: %s", got, expected) + } + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest("POST", "http://example.com/foo"+"?target="+target.URL, strings.NewReader(test.Body.Content)) + q := req.URL.Query() + for k, v := range test.QueryParams { + q.Add(k, v) + } + req.URL.RawQuery = q.Encode() + + recorder := httptest.NewRecorder() + c := config.Config{Body: test.Body} + + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + respBody, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Fatalf("POST body content failed. Got: %s", respBody) + } + target.Close() + } +} diff --git a/config/config.go b/config/config.go index 4f1189e..eb465ec 100644 --- a/config/config.go +++ b/config/config.go @@ -42,6 +42,12 @@ type Config struct { Headers map[string]string `yaml:"headers,omitempty"` Metrics []Metric `yaml:"metrics"` HTTPClientConfig pconfig.HTTPClientConfig `yaml:"http_client_config,omitempty"` + Body ConfigBody `yaml:"body,omitempty"` +} + +type ConfigBody struct { + Content string `yaml:"content"` + Templatize bool `yaml:"templatize,omitempty"` } func LoadConfig(configPath string) (Config, error) { diff --git a/examples/config.yml b/examples/config.yml index a393d70..b4b28f1 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -22,6 +22,17 @@ metrics: headers: X-Dummy: my-test-header +# If 'body' is set, it will be sent by the exporter as the body content in the scrape request. The HTTP method will also be set as 'POST' in this case. +# body: +# content: | +# {"time_diff": "1m25s", "anotherVar": "some value"} + +# The body content can also be a Go Template (https://golang.org/pkg/text/template), with all the functions from the Sprig library (https://masterminds.github.io/sprig/) available. All the query parameters sent by prometheus in the scrape query to the exporter, are available in the template. +# body: +# content: | +# {"time_diff": "{{ duration `95` }}","anotherVar": "{{ .myVal | first }}"} +# templatize: true + # For full http client config parameters, ref: https://pkg.go.dev/github.com/prometheus/common/config?tab=doc#HTTPClientConfig # # http_client_config: diff --git a/exporter/util.go b/exporter/util.go index e3f99fe..4d58f62 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -21,9 +21,12 @@ import ( "io/ioutil" "math" "net/http" + "net/url" "strconv" "strings" + "text/template" + "github.com/Masterminds/sprig/v3" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/prometheus-community/json_exporter/config" @@ -110,21 +113,27 @@ func CreateMetricsList(c config.Config) ([]JsonMetric, error) { return metrics, nil } -func FetchJson(ctx context.Context, logger log.Logger, endpoint string, config config.Config) ([]byte, error) { - httpClientConfig := config.HTTPClientConfig +func FetchJson(ctx context.Context, logger log.Logger, endpoint string, c config.Config, tplValues url.Values) ([]byte, error) { + httpClientConfig := c.HTTPClientConfig client, err := pconfig.NewClientFromConfig(httpClientConfig, "fetch_json", pconfig.WithKeepAlivesDisabled(), pconfig.WithHTTP2Disabled()) if err != nil { level.Error(logger).Log("msg", "Error generating HTTP client", "err", err) //nolint:errcheck return nil, err } - req, err := http.NewRequest("GET", endpoint, nil) + + var req *http.Request + if c.Body.Content == "" { + req, err = http.NewRequest("GET", endpoint, nil) + } else { + req, err = http.NewRequest("POST", endpoint, renderBody(logger, c.Body, tplValues)) + } req = req.WithContext(ctx) if err != nil { level.Error(logger).Log("msg", "Failed to create request", "err", err) //nolint:errcheck return nil, err } - for key, value := range config.Headers { + for key, value := range c.Headers { req.Header.Add(key, value) } if req.Header.Get("Accept") == "" { @@ -153,3 +162,28 @@ func FetchJson(ctx context.Context, logger log.Logger, endpoint string, config c return data, nil } + +// Use the configured template to render the body if enabled +// Do not treat template errors as fatal, on such errors just log them +// and continue with static body content +func renderBody(logger log.Logger, body config.ConfigBody, tplValues url.Values) io.Reader { + br := strings.NewReader(body.Content) + if body.Templatize { + tpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).Parse(body.Content) + if err != nil { + level.Error(logger).Log("msg", "Failed to create a new template from body content", "err", err, "content", body.Content) //nolint:errcheck + return br + } + tpl = tpl.Option("missingkey=zero") + var b strings.Builder + if err := tpl.Execute(&b, tplValues); err != nil { + level.Error(logger).Log("msg", "Failed to render template with values", "err", err, "tempalte", body.Content) //nolint:errcheck + + // `tplValues` can contain sensitive values, so log it only when in debug mode + level.Debug(logger).Log("msg", "Failed to render template with values", "err", err, "tempalte", body.Content, "values", tplValues, "rendered_body", b.String()) //nolint:errcheck + return br + } + br = strings.NewReader(b.String()) + } + return br +} diff --git a/go.mod b/go.mod index 9d61a38..860f7d9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/prometheus-community/json_exporter go 1.14 require ( + github.com/Masterminds/sprig/v3 v3.2.2 github.com/go-kit/kit v0.11.0 github.com/prometheus/client_golang v1.11.0 github.com/prometheus/common v0.30.0 diff --git a/go.sum b/go.sum index 3ffde6f..231741c 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,12 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= @@ -194,6 +200,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -227,9 +234,13 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= @@ -271,10 +282,14 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -348,6 +363,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 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/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -356,6 +373,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -397,6 +416,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=