From 24335ebf1f8eb0996382d98d3dd0b5b0b8c6e760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Sat, 23 Dec 2017 23:24:07 +0100 Subject: [PATCH 1/6] Add support for passing path to custom TLS CA cert & client key/cert pair This adds support for passing a custom http.RoundTripper so in effect we can customize TLS client config for HTTPS requests. Fixes #184 --- docs/CONFIGURATION.md | 28 ++++- docs/example.yaml | 7 ++ internal/alertmanager/models.go | 11 +- internal/alertmanager/tls.go | 55 ++++++++ internal/alertmanager/upstream.go | 25 +++- internal/alertmanager/version.go | 5 +- internal/config/config.go | 1 + internal/config/config_test.go | 4 + internal/config/models.go | 5 + internal/transport/file.go | 5 +- internal/transport/transport.go | 8 +- internal/transport/transport_test.go | 181 ++++++++++++++++++--------- main.go | 20 ++- 13 files changed, 278 insertions(+), 77 deletions(-) create mode 100644 internal/alertmanager/tls.go diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ea69cca9c..55e39ab93 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -44,6 +44,10 @@ alertmanager: uri: string timeout: duration proxy: bool + tls: + ca: string + cert: string + key: string ``` * `interval` - how often alerts should be refreshed, a string in @@ -66,8 +70,20 @@ alertmanager: * `timeout` - timeout for requests send to this Alertmanager server, a string in [time.Duration](https://golang.org/pkg/time/#ParseDuration) format. * `proxy` - if enabled requests from user browsers to this Alertmanager will be - proxied via unsee. This applies to requests made when managing - silences via unsee (creating or expiring silences). + proxied via unsee. This applies to requests made when managing silences via + unsee (creating or expiring silences). +* `tls:ca` - path to CA certificate used to establish TLS connection to this + Alertmanager instance (for URIs using `https://` scheme). If unset or empty + string is set then Go will try to find system CA certificates using well known + paths. +* `tls:cert` - path to a TLS client certificate file to use when establishing + TLS connections to this Alertmanager instance if it requires a TLS client + authentication. + Note that this option requires `tls:key` to be also set. +* `tls:key` - path to a TLS client key file to use when establishing + TLS connections to this Alertmanager instance if it requires a TLS client + authentication. + Note that this option requires `tls:cert` to be also set. Example with two production Alertmanager instances running in HA mode and a staging instance that is also proxied: @@ -88,6 +104,14 @@ alertmanager: uri: https://alertmanager.staging.example.com timeout: 30s proxy: true + tls: + ca: /etc/ssl/staging-ca.crt + - name: protected + uri: https://alertmanager-auth.prod.example.com + timeout: 20s + tls: + cert: /etc/ssl/client.pem + key: /etc/ssl/client.key ``` Defaults: diff --git a/docs/example.yaml b/docs/example.yaml index 043791adb..230fa9891 100644 --- a/docs/example.yaml +++ b/docs/example.yaml @@ -5,6 +5,13 @@ alertmanager: uri: http://localhost:9093 timeout: 10s proxy: true + - name: client-auth + uri: https://localhost:9093 + timeout: 10s + tls: + ca: /etc/ssl/certs/ca-bundle.crt + cert: /etc/unsee/client.pem + key: /etc/unsee/client.key annotations: default: hidden: false diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 9bf41f6a6..8f94e5801 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -3,6 +3,7 @@ package alertmanager import ( "encoding/json" "fmt" + "net/http" "path" "sort" "strings" @@ -37,6 +38,9 @@ type Alertmanager struct { ProxyRequests bool // transport instances are specific to URI scheme we collect from transport transport.Transport + // implements how we fetch requests from the Alertmanager, we don't set it + // by default so it's nil and http.DefaultTransport is used + httpTransport http.RoundTripper // lock protects data access while updating lock sync.RWMutex // fields for storing pulled data @@ -58,15 +62,16 @@ func (am *Alertmanager) detectVersion() string { log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", am.URI, err) return defaultVersion } + ver := alertmanagerVersion{} // read raw body from the source source, err := am.transport.Read(url) - defer source.Close() if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return defaultVersion } + defer source.Close() // decode body as JSON err = json.NewDecoder(source).Decode(&ver) @@ -114,11 +119,11 @@ func (am *Alertmanager) pullSilences(version string) error { start := time.Now() // read raw body from the source source, err := am.transport.Read(url) - defer source.Close() if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return err } + defer source.Close() // decode body text silences, err := mapper.Decode(source) @@ -173,11 +178,11 @@ func (am *Alertmanager) pullAlerts(version string) error { start := time.Now() // read raw body from the source source, err := am.transport.Read(url) - defer source.Close() if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return err } + defer source.Close() // decode body text groups, err := mapper.Decode(source) diff --git a/internal/alertmanager/tls.go b/internal/alertmanager/tls.go new file mode 100644 index 000000000..a3cdf080f --- /dev/null +++ b/internal/alertmanager/tls.go @@ -0,0 +1,55 @@ +package alertmanager + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" +) + +func configureTLSRootCAs(tlsConfig *tls.Config, caPath string) error { + log.Debugf("Loading TLS CA cert '%s'", caPath) + caCert, err := ioutil.ReadFile(caPath) + if err != nil { + return err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = caCertPool + return nil +} + +func configureTLSClientCert(tlsConfig *tls.Config, certPath, keyPath string) error { + log.Debugf("Loading TLS cert '%s' and key '%s'", certPath, keyPath) + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + log.Debugf("Failed to load TLS cert and key: %s", err) + return err + } + tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.BuildNameToCertificate() + return nil +} + +func NewHTTTPTransport(caPath, certPath, keyPath string) (http.RoundTripper, error) { + tlsConfig := &tls.Config{} + + if caPath != "" { + err := configureTLSRootCAs(tlsConfig, caPath) + if err != nil { + return nil, err + } + } + + if certPath != "" { + err := configureTLSClientCert(tlsConfig, certPath, keyPath) + if err != nil { + return nil, err + } + } + + transport := http.Transport{TLSClientConfig: tlsConfig} + return &transport, nil +} diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index a67018811..8ffec1858 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -2,6 +2,7 @@ package alertmanager import ( "fmt" + "net/http" "sync" "time" @@ -12,7 +13,7 @@ import ( ) // Option allows to pass functional options to NewAlertmanager() -type Option func(am *Alertmanager) +type Option func(am *Alertmanager) error var ( upstreams = map[string]*Alertmanager{} @@ -38,11 +39,14 @@ func NewAlertmanager(name, uri string, opts ...Option) (*Alertmanager, error) { } for _, opt := range opts { - opt(am) + err := opt(am) + if err != nil { + return nil, err + } } var err error - am.transport, err = transport.NewTransport(am.URI, am.RequestTimeout) + am.transport, err = transport.NewTransport(am.URI, am.RequestTimeout, am.httpTransport) if err != nil { return am, err } @@ -89,15 +93,26 @@ func GetAlertmanagerByName(name string) *Alertmanager { // WithProxy option can be passed to NewAlertmanager in order to enable request // proxying for unsee clients func WithProxy(proxied bool) Option { - return func(am *Alertmanager) { + return func(am *Alertmanager) error { am.ProxyRequests = proxied + return nil } } // WithRequestTimeout option can be passed to NewAlertmanager in order to set // a custom timeout for Alertmanager upstream requests func WithRequestTimeout(timeout time.Duration) Option { - return func(am *Alertmanager) { + return func(am *Alertmanager) error { am.RequestTimeout = timeout + return nil + } +} + +// WithHTTPTransport option can be passed to NewAlertmanager in order to set +// a custom HTTP transport (http.RoundTripper implementation) +func WithHTTPTransport(httpTransport http.RoundTripper) Option { + return func(am *Alertmanager) error { + am.httpTransport = httpTransport + return nil } } diff --git a/internal/alertmanager/version.go b/internal/alertmanager/version.go index 0fd021d68..09b80e9f0 100644 --- a/internal/alertmanager/version.go +++ b/internal/alertmanager/version.go @@ -1,6 +1,7 @@ package alertmanager import ( + "crypto/tls" "encoding/json" "time" @@ -21,7 +22,7 @@ type alertmanagerVersion struct { } // GetVersion returns version information of the remote Alertmanager endpoint -func GetVersion(uri string, timeout time.Duration) string { +func GetVersion(uri string, timeout time.Duration, tlsConfig *tls.Config) string { // if everything fails assume Alertmanager is at latest possible version defaultVersion := "999.0.0" @@ -32,7 +33,7 @@ func GetVersion(uri string, timeout time.Duration) string { } ver := alertmanagerVersion{} - t, err := transport.NewTransport(uri, timeout) + t, err := transport.NewTransport(uri, timeout, tlsConfig) if err != nil { log.Errorf("Unable to get the version information from %s", url) return defaultVersion diff --git a/internal/config/config.go b/internal/config/config.go index ec19231b9..268b3e7c7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -173,6 +173,7 @@ func (config *configSchema) LogValues() { Name: s.Name, URI: hideURLPassword(s.URI), Timeout: s.Timeout, + TLS: s.TLS, } servers = append(servers, server) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 32e06c37b..dc167385e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -59,6 +59,10 @@ func testReadConfig(t *testing.T) { uri: http://localhost timeout: 40s proxy: false + tls: + ca: "" + cert: "" + key: "" annotations: default: hidden: true diff --git a/internal/config/models.go b/internal/config/models.go index cee46e9bc..32d663af1 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -7,6 +7,11 @@ type alertmanagerConfig struct { URI string Timeout time.Duration Proxy bool + TLS struct { + CA string + Cert string + Key string + } } type jiraRule struct { diff --git a/internal/transport/file.go b/internal/transport/file.go index 6850be4dc..a3d5976d4 100644 --- a/internal/transport/file.go +++ b/internal/transport/file.go @@ -53,6 +53,9 @@ func (t *FileTransport) Read(uri string) (io.ReadCloser, error) { log.Infof("Reading file '%s'", filename) fd, err := os.Open(filename) + if err != nil { + return nil, err + } fr := fileReader{fd: fd} - return &fr, err + return &fr, nil } diff --git a/internal/transport/transport.go b/internal/transport/transport.go index e57d4aa55..53438d756 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -15,7 +15,7 @@ type Transport interface { // NewTransport creates an instance of Transport that can handle URI schema // for the passed uri string -func NewTransport(uri string, timeout time.Duration) (Transport, error) { +func NewTransport(uri string, timeout time.Duration, clientTransport http.RoundTripper) (Transport, error) { u, err := url.Parse(uri) if err != nil { return nil, err @@ -23,7 +23,11 @@ func NewTransport(uri string, timeout time.Duration) (Transport, error) { switch u.Scheme { case "http", "https": - return &HTTPTransport{client: http.Client{Timeout: timeout}}, nil + client := http.Client{ + Timeout: timeout, + Transport: clientTransport, + } + return &HTTPTransport{client: client}, nil case "file": return &FileTransport{}, nil default: diff --git a/internal/transport/transport_test.go b/internal/transport/transport_test.go index 981673f1a..09db233a4 100644 --- a/internal/transport/transport_test.go +++ b/internal/transport/transport_test.go @@ -1,8 +1,13 @@ package transport_test import ( - "encoding/json" + "crypto/tls" + "crypto/x509" "fmt" + "io" + "net/http" + "net/http/httptest" + "os" "testing" "time" @@ -10,95 +15,149 @@ import ( "github.com/cloudflare/unsee/internal/transport" log "github.com/sirupsen/logrus" - httpmock "gopkg.in/jarcoal/httpmock.v1" ) -type transportTest struct { - uri string - timeout time.Duration - failed bool +func getFileSize(path string) int64 { + file, err := os.Open(path) + if err != nil { + log.Fatal(err) + } + fi, err := file.Stat() + if err != nil { + log.Fatal(err) + } + return fi.Size() } -var transportTests = []transportTest{ - transportTest{ - uri: "http://localhost/status", +type httpTransportTest struct { + timeout time.Duration + useTLS bool + tlsConfig *tls.Config + failed bool +} + +var httpTransportTests = []httpTransportTest{ + { + // plain HTTP request, should work }, - transportTest{ - uri: "http://localhost/404", - failed: true, + { + // just enable TLS, will use proper RootCA certs so it should work + useTLS: true, }, - transportTest{ - uri: "http://localhost/invalid", - failed: true, + { + // use empty RootCA pool so we fail on verifying server certificate + useTLS: true, + tlsConfig: &tls.Config{RootCAs: x509.NewCertPool()}, + failed: true, }, - transportTest{ - uri: "https://localhost/status", +} + +type fileTransportTest struct { + uri string + failed bool + timeout time.Duration + size int64 +} + +var fileTransportTests = []fileTransportTest{ + fileTransportTest{ + uri: fmt.Sprintf("file://%s", mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])), + size: getFileSize(mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])), }, - transportTest{ - uri: "https://localhost/404", - failed: true, - }, - transportTest{ - uri: "https://localhost/invalid", - failed: true, - }, - transportTest{ - uri: fmt.Sprintf("file://%s", mock.GetAbsoluteMockPath("status", mock.ListAllMocks()[0])), - }, - transportTest{ + fileTransportTest{ uri: "file:///non-existing-file.abcdef", failed: true, }, - transportTest{ + fileTransportTest{ uri: "file://transport.go", + size: getFileSize("transport.go"), failed: true, }, } -type mockStatus struct { - status string - integer int - yes bool - no bool +func readAll(source io.ReadCloser) int64 { + var readSize int64 + b := make([]byte, 512) + for { + got, err := source.Read(b) + readSize += int64(got) + if err == io.EOF { + break + } + } + return readSize } -func TestTransport(t *testing.T) { +func TestHTTPReader(t *testing.T) { log.SetLevel(log.FatalLevel) - httpmock.Activate() - defer httpmock.DeactivateAndReset() - mockJSON := `{ - "response": "success", - "integer": 123, - "yes": true, - "no": false - }` - httpmock.RegisterResponder("GET", "http://localhost/status", httpmock.NewStringResponder(200, mockJSON)) - httpmock.RegisterResponder("GET", "http://localhost/404", httpmock.NewStringResponder(404, "404")) - httpmock.RegisterResponder("GET", "http://localhost/invalid", httpmock.NewStringResponder(200, "bad json}{}")) - httpmock.RegisterResponder("GET", "https://localhost/status", httpmock.NewStringResponder(200, mockJSON)) - httpmock.RegisterResponder("GET", "https://localhost/404", httpmock.NewStringResponder(404, "404")) - httpmock.RegisterResponder("GET", "https://localhost/invalid", httpmock.NewStringResponder(200, "bad json}{}")) - for _, testCase := range transportTests { - tr, err := transport.NewTransport(testCase.uri, testCase.timeout) - if err != nil { - t.Error(err) + responseBody := "1234" + handler := func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, responseBody) + } + plainTS := httptest.NewServer(http.HandlerFunc(handler)) + defer plainTS.Close() + + tlsTS := httptest.NewTLSServer(http.HandlerFunc(handler)) + + defer tlsTS.Close() + caPool := x509.NewCertPool() + caPool.AddCert(tlsTS.Certificate()) + + for _, testCase := range httpTransportTests { + var uri string + if testCase.useTLS { + uri = tlsTS.URL + } else { + uri = plainTS.URL } - source, err := tr.Read(testCase.uri) + tlsConfig := testCase.tlsConfig + if tlsConfig == nil { + tlsConfig = &tls.Config{RootCAs: caPool} + } + + transp, err := transport.NewTransport(uri, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig}) + if err != nil { + t.Errorf("[%v] failed to create new HTTP transport: %s", testCase, err) + } + + source, err := transp.Read(uri) if err != nil { if !testCase.failed { - t.Errorf("[%s] transport Read() failed with: %s", testCase.uri, err) + t.Errorf("[%v] unexpected failure while creating reader: %s", testCase, err) } continue } - - r := mockStatus{} - err = json.NewDecoder(source).Decode(&r) + got := readAll(source) source.Close() - if (err != nil) != testCase.failed { - t.Errorf("[%s] Expected failure: %v, Read() failed: %v, error: %s", testCase.uri, testCase.failed, (err != nil), err) + if got != int64(len(responseBody)+1) { + t.Errorf("[%v] Wrong respone size, got %d, expected %d", testCase, got, len(responseBody)) + } + } +} + +func TestFileReader(t *testing.T) { + //log.SetLevel(log.FatalLevel) + for _, testCase := range fileTransportTests { + transp, err := transport.NewTransport(testCase.uri, testCase.timeout, &http.Transport{}) + if err != nil { + t.Errorf("[%v] failed to create new transport: %s", testCase, err) + } + + source, err := transp.Read(testCase.uri) + if err != nil { + if !testCase.failed { + t.Errorf("[%v] unexpected failure while creating reader: %s", testCase, err) + } + continue + } + got := readAll(source) + source.Close() + + if got != testCase.size { + t.Errorf("[%v] Wrong respone size, got %d, expected %d", testCase, got, testCase.size) } } } diff --git a/main.go b/main.go index 50f8fc2ce..1c21a107d 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "html/template" + "net/http" "path" "strings" "time" @@ -60,7 +61,24 @@ func setupRouter(router *gin.Engine) { func setupUpstreams() { for _, s := range config.Config.Alertmanager.Servers { - am, err := alertmanager.NewAlertmanager(s.Name, s.URI, alertmanager.WithRequestTimeout(s.Timeout), alertmanager.WithProxy(s.Proxy)) + + var httpTransport http.RoundTripper + var err error + // if either TLS root CA or client cert is configured then initialize custom transport where we have this setup + if s.TLS.CA != "" || s.TLS.Cert != "" { + httpTransport, err = alertmanager.NewHTTTPTransport(s.TLS.CA, s.TLS.Cert, s.TLS.Key) + if err != nil { + log.Fatalf("Failed to create HTTP transport for Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err) + } + } + + am, err := alertmanager.NewAlertmanager( + s.Name, + s.URI, + alertmanager.WithRequestTimeout(s.Timeout), + alertmanager.WithProxy(s.Proxy), + alertmanager.WithHTTPTransport(httpTransport), // we will pass a nil unless TLS.CA or TLS.Cert is set + ) if err != nil { log.Fatalf("Failed to create Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err) } From 338a4c38a6d6c9c10066e2865b2533d6fb4ff478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 22 Jan 2018 23:13:37 -0800 Subject: [PATCH 2/6] Drop duplicated function There's identical method on the Alertmanager instance that's being used --- internal/alertmanager/version.go | 49 -------------------------------- 1 file changed, 49 deletions(-) diff --git a/internal/alertmanager/version.go b/internal/alertmanager/version.go index 09b80e9f0..b001493e0 100644 --- a/internal/alertmanager/version.go +++ b/internal/alertmanager/version.go @@ -1,15 +1,5 @@ package alertmanager -import ( - "crypto/tls" - "encoding/json" - "time" - - "github.com/cloudflare/unsee/internal/transport" - - log "github.com/sirupsen/logrus" -) - // AlertmanagerVersion is what api/v1/status returns, we only use it to check // version, so we skip all other keys (except for status) type alertmanagerVersion struct { @@ -20,42 +10,3 @@ type alertmanagerVersion struct { } `json:"versionInfo"` } `json:"data"` } - -// GetVersion returns version information of the remote Alertmanager endpoint -func GetVersion(uri string, timeout time.Duration, tlsConfig *tls.Config) string { - // if everything fails assume Alertmanager is at latest possible version - defaultVersion := "999.0.0" - - url, err := transport.JoinURL(uri, "api/v1/status") - if err != nil { - log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", uri, err.Error()) - return defaultVersion - } - ver := alertmanagerVersion{} - - t, err := transport.NewTransport(uri, timeout, tlsConfig) - if err != nil { - log.Errorf("Unable to get the version information from %s", url) - return defaultVersion - } - - source, err := t.Read(url) - err = json.NewDecoder(source).Decode(&ver) - if err != nil { - log.Errorf("%s request failed: %s", url, err.Error()) - return defaultVersion - } - - if ver.Status != "success" { - log.Errorf("Request to %s returned status %s", url, ver.Status) - return defaultVersion - } - - if ver.Data.VersionInfo.Version == "" { - log.Error("No version information in Alertmanager API") - return defaultVersion - } - - log.Infof("Remote Alertmanager version: %s", ver.Data.VersionInfo.Version) - return ver.Data.VersionInfo.Version -} From 6f89a77f8deed049409b4aba8095ecfb42da189e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 23 Jan 2018 19:32:39 -0800 Subject: [PATCH 3/6] Rename 'transport' package to 'uri' This package adds handlers for different URI schemes, name clashes with http.Transport which is now passed around. Rename it to make it more obvious what it does --- internal/alertmanager/models.go | 6 +++--- internal/alertmanager/upstream.go | 8 ++++---- internal/mapper/v04/alerts.go | 4 ++-- internal/mapper/v04/silences.go | 4 ++-- internal/mapper/v05/alerts.go | 4 ++-- internal/mapper/v05/silences.go | 4 ++-- internal/mapper/v061/alerts.go | 4 ++-- internal/mapper/v062/alerts.go | 4 ++-- internal/{transport => uri}/file.go | 2 +- internal/{transport => uri}/http.go | 2 +- .../{transport/transport.go => uri/uri.go} | 2 +- .../transport_test.go => uri/uri_test.go} | 20 +++++++++---------- internal/{transport => uri}/urls.go | 2 +- internal/{transport => uri}/urls_test.go | 6 +++--- 14 files changed, 36 insertions(+), 36 deletions(-) rename internal/{transport => uri}/file.go (98%) rename internal/{transport => uri}/http.go (98%) rename internal/{transport/transport.go => uri/uri.go} (97%) rename internal/{transport/transport_test.go => uri/uri_test.go} (87%) rename internal/{transport => uri}/urls.go (95%) rename internal/{transport => uri}/urls_test.go (85%) diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 8f94e5801..39e8f2f79 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -14,7 +14,7 @@ import ( "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" "github.com/cloudflare/unsee/internal/transform" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" log "github.com/sirupsen/logrus" ) @@ -37,7 +37,7 @@ type Alertmanager struct { // whenever this instance should be proxied ProxyRequests bool // transport instances are specific to URI scheme we collect from - transport transport.Transport + transport uri.Transport // implements how we fetch requests from the Alertmanager, we don't set it // by default so it's nil and http.DefaultTransport is used httpTransport http.RoundTripper @@ -57,7 +57,7 @@ func (am *Alertmanager) detectVersion() string { // if everything fails assume Alertmanager is at latest possible version defaultVersion := "999.0.0" - url, err := transport.JoinURL(am.URI, "api/v1/status") + url, err := uri.JoinURL(am.URI, "api/v1/status") if err != nil { log.Errorf("Failed to join url '%s' and path 'api/v1/status': %s", am.URI, err) return defaultVersion diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index 8ffec1858..3246592f9 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -7,7 +7,7 @@ import ( "time" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" log "github.com/sirupsen/logrus" ) @@ -20,9 +20,9 @@ var ( ) // NewAlertmanager creates a new Alertmanager instance -func NewAlertmanager(name, uri string, opts ...Option) (*Alertmanager, error) { +func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, error) { am := &Alertmanager{ - URI: uri, + URI: upstreamURI, RequestTimeout: time.Second * 10, Name: name, lock: sync.RWMutex{}, @@ -46,7 +46,7 @@ func NewAlertmanager(name, uri string, opts ...Option) (*Alertmanager, error) { } var err error - am.transport, err = transport.NewTransport(am.URI, am.RequestTimeout, am.httpTransport) + am.transport, err = uri.NewTransport(am.URI, am.RequestTimeout, am.httpTransport) if err != nil { return am, err } diff --git a/internal/mapper/v04/alerts.go b/internal/mapper/v04/alerts.go index becbffa37..efb09d62f 100644 --- a/internal/mapper/v04/alerts.go +++ b/internal/mapper/v04/alerts.go @@ -15,7 +15,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type alert struct { @@ -56,7 +56,7 @@ type AlertMapper struct { // AbsoluteURL for alerts API endpoint this mapper supports func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/alerts/groups") + return uri.JoinURL(baseURI, "api/v1/alerts/groups") } // IsSupported returns true if given version string is supported diff --git a/internal/mapper/v04/silences.go b/internal/mapper/v04/silences.go index 0362ba4cf..1f8489e7c 100644 --- a/internal/mapper/v04/silences.go +++ b/internal/mapper/v04/silences.go @@ -14,7 +14,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) // Alertmanager 0.4 silence format @@ -49,7 +49,7 @@ type SilenceMapper struct { // AbsoluteURL for silences API endpoint this mapper supports func (m SilenceMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/silences") + return uri.JoinURL(baseURI, "api/v1/silences") } // IsSupported returns true if given version string is supported diff --git a/internal/mapper/v05/alerts.go b/internal/mapper/v05/alerts.go index efc1b1f06..20283cff4 100644 --- a/internal/mapper/v05/alerts.go +++ b/internal/mapper/v05/alerts.go @@ -14,7 +14,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type alert struct { @@ -55,7 +55,7 @@ type AlertMapper struct { // AbsoluteURL for alerts API endpoint this mapper supports func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/alerts/groups") + return uri.JoinURL(baseURI, "api/v1/alerts/groups") } // IsSupported returns true if given version string is supported diff --git a/internal/mapper/v05/silences.go b/internal/mapper/v05/silences.go index eeac798d8..0740df418 100644 --- a/internal/mapper/v05/silences.go +++ b/internal/mapper/v05/silences.go @@ -13,7 +13,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type silence struct { @@ -43,7 +43,7 @@ type SilenceMapper struct { // AbsoluteURL for silences API endpoint this mapper supports func (m SilenceMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/silences") + return uri.JoinURL(baseURI, "api/v1/silences") } // IsSupported returns true if given version string is supported diff --git a/internal/mapper/v061/alerts.go b/internal/mapper/v061/alerts.go index 799759603..c2ff0fa18 100644 --- a/internal/mapper/v061/alerts.go +++ b/internal/mapper/v061/alerts.go @@ -15,7 +15,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type alert struct { @@ -57,7 +57,7 @@ type AlertMapper struct { // AbsoluteURL for alerts API endpoint this mapper supports func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/alerts/groups") + return uri.JoinURL(baseURI, "api/v1/alerts/groups") } // IsSupported returns true if given version string is supported diff --git a/internal/mapper/v062/alerts.go b/internal/mapper/v062/alerts.go index 4d9ba687a..b72f2f5ae 100644 --- a/internal/mapper/v062/alerts.go +++ b/internal/mapper/v062/alerts.go @@ -15,7 +15,7 @@ import ( "github.com/blang/semver" "github.com/cloudflare/unsee/internal/mapper" "github.com/cloudflare/unsee/internal/models" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type alertStatus struct { @@ -61,7 +61,7 @@ type AlertMapper struct { // AbsoluteURL for alerts API endpoint this mapper supports func (m AlertMapper) AbsoluteURL(baseURI string) (string, error) { - return transport.JoinURL(baseURI, "api/v1/alerts/groups") + return uri.JoinURL(baseURI, "api/v1/alerts/groups") } // IsSupported returns true if given version string is supported diff --git a/internal/transport/file.go b/internal/uri/file.go similarity index 98% rename from internal/transport/file.go rename to internal/uri/file.go index a3d5976d4..fa061a3b3 100644 --- a/internal/transport/file.go +++ b/internal/uri/file.go @@ -1,4 +1,4 @@ -package transport +package uri import ( "io" diff --git a/internal/transport/http.go b/internal/uri/http.go similarity index 98% rename from internal/transport/http.go rename to internal/uri/http.go index a30fa7fca..2f8d0cdd1 100644 --- a/internal/transport/http.go +++ b/internal/uri/http.go @@ -1,4 +1,4 @@ -package transport +package uri import ( "compress/gzip" diff --git a/internal/transport/transport.go b/internal/uri/uri.go similarity index 97% rename from internal/transport/transport.go rename to internal/uri/uri.go index 53438d756..406200506 100644 --- a/internal/transport/transport.go +++ b/internal/uri/uri.go @@ -1,4 +1,4 @@ -package transport +package uri import ( "fmt" diff --git a/internal/transport/transport_test.go b/internal/uri/uri_test.go similarity index 87% rename from internal/transport/transport_test.go rename to internal/uri/uri_test.go index 09db233a4..2ad729c6b 100644 --- a/internal/transport/transport_test.go +++ b/internal/uri/uri_test.go @@ -1,4 +1,4 @@ -package transport_test +package uri_test import ( "crypto/tls" @@ -12,7 +12,7 @@ import ( "time" "github.com/cloudflare/unsee/internal/mock" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" log "github.com/sirupsen/logrus" ) @@ -69,8 +69,8 @@ var fileTransportTests = []fileTransportTest{ failed: true, }, fileTransportTest{ - uri: "file://transport.go", - size: getFileSize("transport.go"), + uri: "file://uri.go", + size: getFileSize("uri.go"), failed: true, }, } @@ -105,11 +105,11 @@ func TestHTTPReader(t *testing.T) { caPool.AddCert(tlsTS.Certificate()) for _, testCase := range httpTransportTests { - var uri string + var amURI string if testCase.useTLS { - uri = tlsTS.URL + amURI = tlsTS.URL } else { - uri = plainTS.URL + amURI = plainTS.URL } tlsConfig := testCase.tlsConfig @@ -117,12 +117,12 @@ func TestHTTPReader(t *testing.T) { tlsConfig = &tls.Config{RootCAs: caPool} } - transp, err := transport.NewTransport(uri, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig}) + transp, err := uri.NewTransport(amURI, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig}) if err != nil { t.Errorf("[%v] failed to create new HTTP transport: %s", testCase, err) } - source, err := transp.Read(uri) + source, err := transp.Read(amURI) if err != nil { if !testCase.failed { t.Errorf("[%v] unexpected failure while creating reader: %s", testCase, err) @@ -141,7 +141,7 @@ func TestHTTPReader(t *testing.T) { func TestFileReader(t *testing.T) { //log.SetLevel(log.FatalLevel) for _, testCase := range fileTransportTests { - transp, err := transport.NewTransport(testCase.uri, testCase.timeout, &http.Transport{}) + transp, err := uri.NewTransport(testCase.uri, testCase.timeout, &http.Transport{}) if err != nil { t.Errorf("[%v] failed to create new transport: %s", testCase, err) } diff --git a/internal/transport/urls.go b/internal/uri/urls.go similarity index 95% rename from internal/transport/urls.go rename to internal/uri/urls.go index 374e746ba..b9f0b416d 100644 --- a/internal/transport/urls.go +++ b/internal/uri/urls.go @@ -1,4 +1,4 @@ -package transport +package uri import ( "net/url" diff --git a/internal/transport/urls_test.go b/internal/uri/urls_test.go similarity index 85% rename from internal/transport/urls_test.go rename to internal/uri/urls_test.go index 9e3c715ed..9e7f97636 100644 --- a/internal/transport/urls_test.go +++ b/internal/uri/urls_test.go @@ -1,9 +1,9 @@ -package transport_test +package uri_test import ( "testing" - "github.com/cloudflare/unsee/internal/transport" + "github.com/cloudflare/unsee/internal/uri" ) type joinURLTest struct { @@ -32,7 +32,7 @@ var joinURLTests = []joinURLTest{ func TestJoinURL(t *testing.T) { for _, testCase := range joinURLTests { - url, err := transport.JoinURL(testCase.base, testCase.sub) + url, err := uri.JoinURL(testCase.base, testCase.sub) if err != nil { t.Errorf("joinURL(%v, %v) failed: %s", testCase.base, testCase.sub, err.Error()) } From d5a7cb9ace5c3fa4b956d8f8a71d77523a8e69c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 23 Jan 2018 19:40:24 -0800 Subject: [PATCH 4/6] Rename Transport interface to Reader --- internal/alertmanager/models.go | 10 +++++----- internal/alertmanager/upstream.go | 2 +- internal/uri/file.go | 10 +++++----- internal/uri/http.go | 10 +++++----- internal/uri/uri.go | 12 ++++++------ internal/uri/uri_test.go | 4 ++-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 39e8f2f79..de69f4423 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -36,8 +36,8 @@ type Alertmanager struct { Name string `json:"name"` // whenever this instance should be proxied ProxyRequests bool - // transport instances are specific to URI scheme we collect from - transport uri.Transport + // reader instances are specific to URI scheme we collect from + reader uri.Reader // implements how we fetch requests from the Alertmanager, we don't set it // by default so it's nil and http.DefaultTransport is used httpTransport http.RoundTripper @@ -66,7 +66,7 @@ func (am *Alertmanager) detectVersion() string { ver := alertmanagerVersion{} // read raw body from the source - source, err := am.transport.Read(url) + source, err := am.reader.Read(url) if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return defaultVersion @@ -118,7 +118,7 @@ func (am *Alertmanager) pullSilences(version string) error { start := time.Now() // read raw body from the source - source, err := am.transport.Read(url) + source, err := am.reader.Read(url) if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return err @@ -177,7 +177,7 @@ func (am *Alertmanager) pullAlerts(version string) error { start := time.Now() // read raw body from the source - source, err := am.transport.Read(url) + source, err := am.reader.Read(url) if err != nil { log.Errorf("[%s] %s request failed: %s", am.Name, url, err) return err diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index 3246592f9..610349363 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -46,7 +46,7 @@ func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, e } var err error - am.transport, err = uri.NewTransport(am.URI, am.RequestTimeout, am.httpTransport) + am.reader, err = uri.NewReader(am.URI, am.RequestTimeout, am.httpTransport) if err != nil { return am, err } diff --git a/internal/uri/file.go b/internal/uri/file.go index fa061a3b3..aef474773 100644 --- a/internal/uri/file.go +++ b/internal/uri/file.go @@ -22,11 +22,11 @@ func (fr *fileReader) Close() error { return fr.fd.Close() } -// FileTransport can read data from file:// URIs -type FileTransport struct { +// FileURIReader can read data from file:// URIs +type FileURIReader struct { } -func (t *FileTransport) pathFromURI(uri string) (string, error) { +func (r *FileURIReader) pathFromURI(uri string) (string, error) { u, err := url.Parse(uri) if err != nil { return "", err @@ -45,8 +45,8 @@ func (t *FileTransport) pathFromURI(uri string) (string, error) { return absolutePath, nil } -func (t *FileTransport) Read(uri string) (io.ReadCloser, error) { - filename, err := t.pathFromURI(uri) +func (r *FileURIReader) Read(uri string) (io.ReadCloser, error) { + filename, err := r.pathFromURI(uri) if err != nil { return nil, err } diff --git a/internal/uri/http.go b/internal/uri/http.go index 2f8d0cdd1..a4ced4fc0 100644 --- a/internal/uri/http.go +++ b/internal/uri/http.go @@ -9,13 +9,13 @@ import ( log "github.com/sirupsen/logrus" ) -// HTTPTransport can read data from http:// and https:// URIs -type HTTPTransport struct { +// HTTPURIReader can read data from http:// and https:// URIs +type HTTPURIReader struct { client http.Client } -func (t *HTTPTransport) Read(uri string) (io.ReadCloser, error) { - log.Infof("GET %s timeout=%s", uri, t.client.Timeout) +func (r *HTTPURIReader) Read(uri string) (io.ReadCloser, error) { + log.Infof("GET %s timeout=%s", uri, r.client.Timeout) request, err := http.NewRequest("GET", uri, nil) if err != nil { @@ -23,7 +23,7 @@ func (t *HTTPTransport) Read(uri string) (io.ReadCloser, error) { } request.Header.Add("Accept-Encoding", "gzip") - resp, err := t.client.Do(request) + resp, err := r.client.Do(request) if err != nil { return nil, err } diff --git a/internal/uri/uri.go b/internal/uri/uri.go index 406200506..1c0217e6d 100644 --- a/internal/uri/uri.go +++ b/internal/uri/uri.go @@ -8,14 +8,14 @@ import ( "time" ) -// Transport reads from a specific URI schema -type Transport interface { +// Reader reads from a specific URI schema +type Reader interface { Read(string) (io.ReadCloser, error) } -// NewTransport creates an instance of Transport that can handle URI schema +// NewReader creates an instance of URIReader that can handle URI schema // for the passed uri string -func NewTransport(uri string, timeout time.Duration, clientTransport http.RoundTripper) (Transport, error) { +func NewReader(uri string, timeout time.Duration, clientTransport http.RoundTripper) (Reader, error) { u, err := url.Parse(uri) if err != nil { return nil, err @@ -27,9 +27,9 @@ func NewTransport(uri string, timeout time.Duration, clientTransport http.RoundT Timeout: timeout, Transport: clientTransport, } - return &HTTPTransport{client: client}, nil + return &HTTPURIReader{client: client}, nil case "file": - return &FileTransport{}, nil + return &FileURIReader{}, nil default: return nil, fmt.Errorf("Unsupported URI scheme '%s' in '%s'", u.Scheme, u) } diff --git a/internal/uri/uri_test.go b/internal/uri/uri_test.go index 2ad729c6b..0e8dfea7e 100644 --- a/internal/uri/uri_test.go +++ b/internal/uri/uri_test.go @@ -117,7 +117,7 @@ func TestHTTPReader(t *testing.T) { tlsConfig = &tls.Config{RootCAs: caPool} } - transp, err := uri.NewTransport(amURI, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig}) + transp, err := uri.NewReader(amURI, testCase.timeout, &http.Transport{TLSClientConfig: tlsConfig}) if err != nil { t.Errorf("[%v] failed to create new HTTP transport: %s", testCase, err) } @@ -141,7 +141,7 @@ func TestHTTPReader(t *testing.T) { func TestFileReader(t *testing.T) { //log.SetLevel(log.FatalLevel) for _, testCase := range fileTransportTests { - transp, err := uri.NewTransport(testCase.uri, testCase.timeout, &http.Transport{}) + transp, err := uri.NewReader(testCase.uri, testCase.timeout, &http.Transport{}) if err != nil { t.Errorf("[%v] failed to create new transport: %s", testCase, err) } From 49d1ed2e1e0c0d012ae34d85fbf2055beef176b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 23 Jan 2018 21:37:56 -0800 Subject: [PATCH 5/6] Pass HTTP transport to the proxy When we proxy requests we should use same transport as we use when collecting alerts & silences --- internal/alertmanager/models.go | 2 +- internal/alertmanager/upstream.go | 4 ++-- proxy.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index de69f4423..012501774 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -40,7 +40,7 @@ type Alertmanager struct { reader uri.Reader // implements how we fetch requests from the Alertmanager, we don't set it // by default so it's nil and http.DefaultTransport is used - httpTransport http.RoundTripper + HTTPTransport http.RoundTripper `json:"-"` // lock protects data access while updating lock sync.RWMutex // fields for storing pulled data diff --git a/internal/alertmanager/upstream.go b/internal/alertmanager/upstream.go index 610349363..1da5b0def 100644 --- a/internal/alertmanager/upstream.go +++ b/internal/alertmanager/upstream.go @@ -46,7 +46,7 @@ func NewAlertmanager(name, upstreamURI string, opts ...Option) (*Alertmanager, e } var err error - am.reader, err = uri.NewReader(am.URI, am.RequestTimeout, am.httpTransport) + am.reader, err = uri.NewReader(am.URI, am.RequestTimeout, am.HTTPTransport) if err != nil { return am, err } @@ -112,7 +112,7 @@ func WithRequestTimeout(timeout time.Duration) Option { // a custom HTTP transport (http.RoundTripper implementation) func WithHTTPTransport(httpTransport http.RoundTripper) Option { return func(am *Alertmanager) error { - am.httpTransport = httpTransport + am.HTTPTransport = httpTransport return nil } } diff --git a/proxy.go b/proxy.go index e11f83d3d..a8e26d93f 100644 --- a/proxy.go +++ b/proxy.go @@ -37,6 +37,7 @@ func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) (*httputil.Re req.Header.Del("Accept-Encoding") log.Debugf("[%s] Proxy request for %s", alertmanager.Name, req.URL.Path) }, + Transport: alertmanager.HTTPTransport, ModifyResponse: func(resp *http.Response) error { // drop Content-Length header from upstream responses, gzip middleware // will compress those and that could cause a mismatch From a0cf3cc43c79d6c7a8e2ef2124ecae9de658d56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 23 Jan 2018 21:38:46 -0800 Subject: [PATCH 6/6] Add a JSON tag for ProxyRequests attr --- internal/alertmanager/models.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/alertmanager/models.go b/internal/alertmanager/models.go index 012501774..7f3582cf0 100644 --- a/internal/alertmanager/models.go +++ b/internal/alertmanager/models.go @@ -35,7 +35,7 @@ type Alertmanager struct { RequestTimeout time.Duration `json:"timeout"` Name string `json:"name"` // whenever this instance should be proxied - ProxyRequests bool + ProxyRequests bool `json:"proxyRequests"` // reader instances are specific to URI scheme we collect from reader uri.Reader // implements how we fetch requests from the Alertmanager, we don't set it