From 7962b7d2b677619f8ef36513cf30b33205b6a8c4 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Mon, 18 Dec 2017 14:30:37 +0100 Subject: [PATCH 1/2] Add auth options for WebHook notifier This change adds common authentication options to the WebHook notifier: - Bearer token - TLS client certificate - Basic authentication It also adds TLS options for CA certificate, server name and disabling validation of server-side certificates. --- config/notifiers.go | 27 +++++++++++++++++++ notify/impl.go | 64 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/config/notifiers.go b/config/notifiers.go index e92736a752..5320c1d070 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -297,11 +297,35 @@ type WebhookConfig struct { // URL to send POST request to. URL string `yaml:"url" json:"url"` + // Optional TLS configuration + TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` + // Optional bearer token + BearerToken Secret `yaml:"bearer_token,omitempty" json:"bearer_token,omitempty"` + // Optional bearer token file + BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"` + // TODO: Optional BasicAuth configuration // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } +// TLSConfig configures the options for TLS connections. +type TLSConfig struct { + // The CA cert file. + CAFile string `yaml:"ca_file,omitempty"` + // The client cert file. + CertFile string `yaml:"cert_file,omitempty"` + // The client key file. + KeyFile string `yaml:"key_file,omitempty"` + // Used to verify the server's hostname. + ServerName string `yaml:"server_name,omitempty"` + // Disable certificate validation. + InsecureSkipVerify bool `yaml:"insecure_skip_verify"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultWebhookConfig @@ -320,6 +344,9 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("scheme required for webhook url") } c.URL = url.String() + if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { + return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured") + } return checkOverflow(c.XXX, "webhook config") } diff --git a/notify/impl.go b/notify/impl.go index 506acdf39a..57629300ad 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -17,6 +17,7 @@ import ( "bytes" "crypto/sha256" "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -147,14 +148,68 @@ var userAgentHeader = fmt.Sprintf("Alertmanager/%s", version.Version) // Webhook implements a Notifier for generic webhooks. type Webhook struct { // The URL to which notifications are sent. - URL string + URL string + // Optional bearer token + token string + client *http.Client tmpl *template.Template logger log.Logger } // NewWebhook returns a new Webhook. func NewWebhook(conf *config.WebhookConfig, t *template.Template, l log.Logger) *Webhook { - return &Webhook{URL: conf.URL, tmpl: t, logger: l} + var token string + if len(conf.BearerToken) > 0 { + token = string(conf.BearerToken) + } else if len(conf.BearerTokenFile) > 0 { + bf, err := ioutil.ReadFile(conf.BearerTokenFile) + if err == nil { + token = string(bf) + } else { + level.Error(l).Log("msg", "Failed to read bearer token file", "token_file", conf.BearerTokenFile, "err", err) + } + } + + return &Webhook{URL: conf.URL, token: token, client: newHttpClient(&conf.TLSConfig, l), tmpl: t, logger: l} +} + +func newHttpClient(conf *config.TLSConfig, l log.Logger) *http.Client { + tlsConfig := &tls.Config{InsecureSkipVerify: conf.InsecureSkipVerify} + if len(conf.CAFile) > 0 { + caCertPool := x509.NewCertPool() + caCert, err := ioutil.ReadFile(conf.CAFile) + if err == nil { + if caCertPool.AppendCertsFromPEM(caCert) { + tlsConfig.RootCAs = caCertPool + } else { + level.Error(l).Log("msg", "Failed to add CA cert", "cert_file", conf.CAFile) + } + } else { + level.Error(l).Log("msg", "Failed to read CA cert", "cert_file", conf.CAFile, "err", err) + } + } + + if len(conf.ServerName) > 0 { + tlsConfig.ServerName = conf.ServerName + } + + if len(conf.CertFile) > 0 && len(conf.KeyFile) > 0 { + cert, err := tls.LoadX509KeyPair(conf.CertFile, conf.KeyFile) + if err == nil { + tlsConfig.Certificates = []tls.Certificate{cert} + } else { + level.Error(l).Log("msg", "Unable to use client cert", "cert_file", conf.CertFile, "key_file", conf.KeyFile, "err", err) + } + } + tlsConfig.BuildNameToCertificate() + + return &http.Client{ + Timeout: time.Second * 10, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + TLSHandshakeTimeout: 5 * time.Second, + }, + } } // WebhookMessage defines the JSON object send to webhook endpoints. @@ -192,8 +247,11 @@ func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) (bool, err } req.Header.Set("Content-Type", contentTypeJSON) req.Header.Set("User-Agent", userAgentHeader) + if w.token != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.token)) + } - resp, err := ctxhttp.Do(ctx, http.DefaultClient, req) + resp, err := ctxhttp.Do(ctx, w.client, req) if err != nil { return true, err } From 7d3288d0060889c64ffcb30a49640f70d78e18a9 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Tue, 19 Dec 2017 10:07:35 +0100 Subject: [PATCH 2/2] Rework change to use code from prometheus/common This requires to pull github.com/prometheus/common/config in vendor. --- config/notifiers.go | 31 +- notify/impl.go | 65 +--- .../prometheus/common/config/config.go | 47 +++ .../prometheus/common/config/http_config.go | 279 ++++++++++++++++++ vendor/vendor.json | 6 + 5 files changed, 345 insertions(+), 83 deletions(-) create mode 100644 vendor/github.com/prometheus/common/config/config.go create mode 100644 vendor/github.com/prometheus/common/config/http_config.go diff --git a/config/notifiers.go b/config/notifiers.go index 5320c1d070..96f8e446a6 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -18,6 +18,8 @@ import ( "net/url" "strings" "time" + + pconfig "github.com/prometheus/common/config" ) var ( @@ -297,35 +299,13 @@ type WebhookConfig struct { // URL to send POST request to. URL string `yaml:"url" json:"url"` - // Optional TLS configuration - TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` - // Optional bearer token - BearerToken Secret `yaml:"bearer_token,omitempty" json:"bearer_token,omitempty"` - // Optional bearer token file - BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"` - // TODO: Optional BasicAuth configuration + // HTTP client parameters. + HTTPClientConfig pconfig.HTTPClientConfig `yaml:"http_client_config,inline" json:"http_client_config,inline"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline" json:"-"` } -// TLSConfig configures the options for TLS connections. -type TLSConfig struct { - // The CA cert file. - CAFile string `yaml:"ca_file,omitempty"` - // The client cert file. - CertFile string `yaml:"cert_file,omitempty"` - // The client key file. - KeyFile string `yaml:"key_file,omitempty"` - // Used to verify the server's hostname. - ServerName string `yaml:"server_name,omitempty"` - // Disable certificate validation. - InsecureSkipVerify bool `yaml:"insecure_skip_verify"` - - // Catches all undefined fields and must be empty after parsing. - XXX map[string]interface{} `yaml:",inline"` -} - // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultWebhookConfig @@ -344,9 +324,6 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("scheme required for webhook url") } c.URL = url.String() - if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { - return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured") - } return checkOverflow(c.XXX, "webhook config") } diff --git a/notify/impl.go b/notify/impl.go index 57629300ad..06c29d7865 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -17,7 +17,6 @@ import ( "bytes" "crypto/sha256" "crypto/tls" - "crypto/x509" "encoding/json" "errors" "fmt" @@ -35,6 +34,7 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" + pconfig "github.com/prometheus/common/config" "github.com/prometheus/common/model" "github.com/prometheus/common/version" "golang.org/x/net/context" @@ -148,9 +148,7 @@ var userAgentHeader = fmt.Sprintf("Alertmanager/%s", version.Version) // Webhook implements a Notifier for generic webhooks. type Webhook struct { // The URL to which notifications are sent. - URL string - // Optional bearer token - token string + URL string client *http.Client tmpl *template.Template logger log.Logger @@ -158,58 +156,16 @@ type Webhook struct { // NewWebhook returns a new Webhook. func NewWebhook(conf *config.WebhookConfig, t *template.Template, l log.Logger) *Webhook { - var token string - if len(conf.BearerToken) > 0 { - token = string(conf.BearerToken) - } else if len(conf.BearerTokenFile) > 0 { - bf, err := ioutil.ReadFile(conf.BearerTokenFile) - if err == nil { - token = string(bf) - } else { - level.Error(l).Log("msg", "Failed to read bearer token file", "token_file", conf.BearerTokenFile, "err", err) - } - } - - return &Webhook{URL: conf.URL, token: token, client: newHttpClient(&conf.TLSConfig, l), tmpl: t, logger: l} -} - -func newHttpClient(conf *config.TLSConfig, l log.Logger) *http.Client { - tlsConfig := &tls.Config{InsecureSkipVerify: conf.InsecureSkipVerify} - if len(conf.CAFile) > 0 { - caCertPool := x509.NewCertPool() - caCert, err := ioutil.ReadFile(conf.CAFile) - if err == nil { - if caCertPool.AppendCertsFromPEM(caCert) { - tlsConfig.RootCAs = caCertPool - } else { - level.Error(l).Log("msg", "Failed to add CA cert", "cert_file", conf.CAFile) - } - } else { - level.Error(l).Log("msg", "Failed to read CA cert", "cert_file", conf.CAFile, "err", err) - } - } + w := &Webhook{URL: conf.URL, client: http.DefaultClient, tmpl: t, logger: l} - if len(conf.ServerName) > 0 { - tlsConfig.ServerName = conf.ServerName - } - - if len(conf.CertFile) > 0 && len(conf.KeyFile) > 0 { - cert, err := tls.LoadX509KeyPair(conf.CertFile, conf.KeyFile) - if err == nil { - tlsConfig.Certificates = []tls.Certificate{cert} - } else { - level.Error(l).Log("msg", "Unable to use client cert", "cert_file", conf.CertFile, "key_file", conf.KeyFile, "err", err) - } + client, err := pconfig.NewHTTPClientFromConfig(&conf.HTTPClientConfig) + if err != nil { + level.Error(l).Log("msg", "Failed to create HTTP client for webhook", "err", err, "url", conf.URL) + } else { + w.client = client } - tlsConfig.BuildNameToCertificate() - return &http.Client{ - Timeout: time.Second * 10, - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - TLSHandshakeTimeout: 5 * time.Second, - }, - } + return w } // WebhookMessage defines the JSON object send to webhook endpoints. @@ -247,9 +203,6 @@ func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) (bool, err } req.Header.Set("Content-Type", contentTypeJSON) req.Header.Set("User-Agent", userAgentHeader) - if w.token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.token)) - } resp, err := ctxhttp.Do(ctx, w.client, req) if err != nil { diff --git a/vendor/github.com/prometheus/common/config/config.go b/vendor/github.com/prometheus/common/config/config.go new file mode 100644 index 0000000000..9195c34bfd --- /dev/null +++ b/vendor/github.com/prometheus/common/config/config.go @@ -0,0 +1,47 @@ +// Copyright 2016 The Prometheus 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 config + +import ( + "fmt" + "strings" +) + +func checkOverflow(m map[string]interface{}, ctx string) error { + if len(m) > 0 { + var keys []string + for k := range m { + keys = append(keys, k) + } + return fmt.Errorf("unknown fields in %s: %s", ctx, strings.Join(keys, ", ")) + } + return nil +} + +// Secret special type for storing secrets. +type Secret string + +// MarshalYAML implements the yaml.Marshaler interface for Secrets. +func (s Secret) MarshalYAML() (interface{}, error) { + if s != "" { + return "", nil + } + return nil, nil +} + +//UnmarshalYAML implements the yaml.Unmarshaler interface for Secrets. +func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain Secret + return unmarshal((*plain)(s)) +} diff --git a/vendor/github.com/prometheus/common/config/http_config.go b/vendor/github.com/prometheus/common/config/http_config.go new file mode 100644 index 0000000000..ff5837fa5e --- /dev/null +++ b/vendor/github.com/prometheus/common/config/http_config.go @@ -0,0 +1,279 @@ +// Copyright 2016 The Prometheus 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 config + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + yaml "gopkg.in/yaml.v2" +) + +// BasicAuth contains basic HTTP authentication credentials. +type BasicAuth struct { + Username string `yaml:"username"` + Password Secret `yaml:"password"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// URL is a custom URL type that allows validation at configuration load time. +type URL struct { + *url.URL +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface for URLs. +func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + urlp, err := url.Parse(s) + if err != nil { + return err + } + u.URL = urlp + return nil +} + +// MarshalYAML implements the yaml.Marshaler interface for URLs. +func (u URL) MarshalYAML() (interface{}, error) { + if u.URL != nil { + return u.String(), nil + } + return nil, nil +} + +// HTTPClientConfig configures an HTTP client. +type HTTPClientConfig struct { + // The HTTP basic authentication credentials for the targets. + BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` + // The bearer token for the targets. + BearerToken Secret `yaml:"bearer_token,omitempty"` + // The bearer token file for the targets. + BearerTokenFile string `yaml:"bearer_token_file,omitempty"` + // HTTP proxy server to use to connect to the targets. + ProxyURL URL `yaml:"proxy_url,omitempty"` + // TLSConfig to use to connect to the targets. + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +func (c *HTTPClientConfig) validate() error { + if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { + return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured") + } + if c.BasicAuth != nil && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) { + return fmt.Errorf("at most one of basic_auth, bearer_token & bearer_token_file must be configured") + } + return nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface +func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain HTTPClientConfig + err := unmarshal((*plain)(c)) + if err != nil { + return err + } + err = c.validate() + if err != nil { + return c.validate() + } + return checkOverflow(c.XXX, "http_client_config") +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (a *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain BasicAuth + err := unmarshal((*plain)(a)) + if err != nil { + return err + } + return checkOverflow(a.XXX, "basic_auth") +} + +// NewHTTPClientFromConfig returns a new HTTP client configured for the +// given config.HTTPClientConfig. +func NewHTTPClientFromConfig(cfg *HTTPClientConfig) (*http.Client, error) { + tlsConfig, err := NewTLSConfig(&cfg.TLSConfig) + if err != nil { + return nil, err + } + + // It's the caller's job to handle timeouts + var rt http.RoundTripper = &http.Transport{ + Proxy: http.ProxyURL(cfg.ProxyURL.URL), + DisableKeepAlives: true, + TLSClientConfig: tlsConfig, + } + + // If a bearer token is provided, create a round tripper that will set the + // Authorization header correctly on each request. + bearerToken := cfg.BearerToken + if len(bearerToken) == 0 && len(cfg.BearerTokenFile) > 0 { + b, err := ioutil.ReadFile(cfg.BearerTokenFile) + if err != nil { + return nil, fmt.Errorf("unable to read bearer token file %s: %s", cfg.BearerTokenFile, err) + } + bearerToken = Secret(strings.TrimSpace(string(b))) + } + + if len(bearerToken) > 0 { + rt = NewBearerAuthRoundTripper(bearerToken, rt) + } + + if cfg.BasicAuth != nil { + rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, Secret(cfg.BasicAuth.Password), rt) + } + + // Return a new client with the configured round tripper. + return &http.Client{Transport: rt}, nil +} + +type bearerAuthRoundTripper struct { + bearerToken Secret + rt http.RoundTripper +} + +type basicAuthRoundTripper struct { + username string + password Secret + rt http.RoundTripper +} + +// NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has +// already been set. +func NewBasicAuthRoundTripper(username string, password Secret, rt http.RoundTripper) http.RoundTripper { + return &basicAuthRoundTripper{username, password, rt} +} + +func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if len(req.Header.Get("Authorization")) == 0 { + req = cloneRequest(req) + req.Header.Set("Authorization", "Bearer "+string(rt.bearerToken)) + } + + return rt.rt.RoundTrip(req) +} + +// NewBearerAuthRoundTripper adds the provided bearer token to a request unless the authorization +// header has already been set. +func NewBearerAuthRoundTripper(bearer Secret, rt http.RoundTripper) http.RoundTripper { + return &bearerAuthRoundTripper{bearer, rt} +} + +func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if len(req.Header.Get("Authorization")) != 0 { + return rt.RoundTrip(req) + } + req = cloneRequest(req) + req.SetBasicAuth(rt.username, string(rt.password)) + return rt.rt.RoundTrip(req) +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // Shallow copy of the struct. + r2 := new(http.Request) + *r2 = *r + // Deep copy of the Header. + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} + +// NewTLSConfig creates a new tls.Config from the given config.TLSConfig. +func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) { + tlsConfig := &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify} + + // If a CA cert is provided then let's read it in so we can validate the + // scrape target's certificate properly. + if len(cfg.CAFile) > 0 { + caCertPool := x509.NewCertPool() + // Load CA cert. + caCert, err := ioutil.ReadFile(cfg.CAFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified CA cert %s: %s", cfg.CAFile, err) + } + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = caCertPool + } + + if len(cfg.ServerName) > 0 { + tlsConfig.ServerName = cfg.ServerName + } + + // If a client cert & key is provided then configure TLS config accordingly. + if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 { + return nil, fmt.Errorf("client cert file %q specified without client key file", cfg.CertFile) + } else if len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0 { + return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile) + } else if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 { + cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", cfg.CertFile, cfg.KeyFile, err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + tlsConfig.BuildNameToCertificate() + + return tlsConfig, nil +} + +// TLSConfig configures the options for TLS connections. +type TLSConfig struct { + // The CA cert to use for the targets. + CAFile string `yaml:"ca_file,omitempty"` + // The client cert file for the targets. + CertFile string `yaml:"cert_file,omitempty"` + // The client key file for the targets. + KeyFile string `yaml:"key_file,omitempty"` + // Used to verify the hostname for the targets. + ServerName string `yaml:"server_name,omitempty"` + // Disable target certificate validation. + InsecureSkipVerify bool `yaml:"insecure_skip_verify"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *TLSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain TLSConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + return checkOverflow(c.XXX, "TLS config") +} + +func (c HTTPClientConfig) String() string { + b, err := yaml.Marshal(c) + if err != nil { + return fmt.Sprintf("", err) + } + return string(b) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 53ebded71f..91b4a01fe0 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -250,6 +250,12 @@ "revision": "1e01b2bdbae8edb393fcf555732304f34d192fc9", "revisionTime": "2016-11-21T14:22:35Z" }, + { + "checksumSHA1": "4TLgSCgJZuS5gtytxNvcVk4h8/g=", + "path": "github.com/prometheus/common/config", + "revision": "2e54d0b93cba2fd133edc32211dcc32c06ef72ca", + "revisionTime": "2017-11-17T16:30:51Z" + }, { "checksumSHA1": "xfnn0THnqNwjwimeTClsxahYrIo=", "path": "github.com/prometheus/common/expfmt",