From 8d972ed4312ed6337878d56e05cf7e5b57bed0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 22:54:10 -0700 Subject: [PATCH 1/7] Add a helper function GetAbsoluteMockPath Split this code into a dedicated function, will use later --- mock/mock.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mock/mock.go b/mock/mock.go index 98ea79169..f5cef46bc 100644 --- a/mock/mock.go +++ b/mock/mock.go @@ -9,11 +9,16 @@ import ( httpmock "gopkg.in/jarcoal/httpmock.v1" ) -// RegisterURL for given url and return 200 status register mock http responder -func RegisterURL(url string, version string, filename string) { +// GetAbsoluteMockPath returns absolute path for given mock file +func GetAbsoluteMockPath(filename string, version string) string { _, f, _, _ := runtime.Caller(0) cwd := filepath.Dir(f) - fullPath := fmt.Sprintf("%s/%s/api/v1/%s", cwd, version, filename) + return fmt.Sprintf("%s/%s/api/v1/%s", cwd, version, filename) +} + +// RegisterURL for given url and return 200 status register mock http responder +func RegisterURL(url string, version string, filename string) { + fullPath := GetAbsoluteMockPath(filename, version) mockJSON, err := ioutil.ReadFile(fullPath) if err != nil { panic(err) From 436017b03282a271c197eb950d69482a136fcfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 22:55:37 -0700 Subject: [PATCH 2/7] Add file transport reader This simply returns os.File reader --- transport/file.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 transport/file.go diff --git a/transport/file.go b/transport/file.go new file mode 100644 index 000000000..8fd726703 --- /dev/null +++ b/transport/file.go @@ -0,0 +1,16 @@ +package transport + +import ( + "os" + + log "github.com/Sirupsen/logrus" +) + +type fileReader struct { + filename string +} + +func newFileReader(filname string) (*os.File, error) { + log.Infof("Reading file '%s'", filname) + return os.Open(filname) +} From 781d2cbd69d20527ae0596db77155cfaebece8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 22:58:03 -0700 Subject: [PATCH 3/7] Add http transport reader Copy code from GetJSONFromURL into a http reader constructor --- transport/http.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/transport/http.go b/transport/http.go index 15a1bf7c0..b438f0b48 100644 --- a/transport/http.go +++ b/transport/http.go @@ -50,3 +50,47 @@ func GetJSONFromURL(url string, timeout time.Duration, target interface{}) error return json.NewDecoder(reader).Decode(target) } + +type httpReader struct { + URL string + Timeout time.Duration +} + +func newHTTPReader(url string, timeout time.Duration) (*io.ReadCloser, error) { + hr := httpReader{URL: url, Timeout: timeout} + + log.Infof("GET %s timeout=%s", hr.URL, hr.Timeout) + + c := &http.Client{ + Timeout: timeout, + } + + req, err := http.NewRequest("GET", hr.URL, nil) + if err != nil { + return nil, err + } + req.Header.Add("Accept-Encoding", "gzip") + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Request to Alertmanager failed with %s", resp.Status) + } + + defer resp.Body.Close() + + var reader io.ReadCloser + switch resp.Header.Get("Content-Encoding") { + case "gzip": + reader, err = gzip.NewReader(resp.Body) + if err != nil { + return nil, fmt.Errorf("Failed to decode gzipped content: %s", err.Error()) + } + defer reader.Close() + default: + reader = resp.Body + } + return &reader, nil +} From 940eb77c69c0cbc5c623ebf9288ef3da93328458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 23:01:32 -0700 Subject: [PATCH 4/7] Add ReadJSON that will replace GetJSONFromURL --- transport/transport.go | 39 ++++++++++++++++ transport/transport_test.go | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 transport/transport.go create mode 100644 transport/transport_test.go diff --git a/transport/transport.go b/transport/transport.go new file mode 100644 index 000000000..1a0a4025f --- /dev/null +++ b/transport/transport.go @@ -0,0 +1,39 @@ +package transport + +import ( + "encoding/json" + "fmt" + "net/url" + "time" +) + +func readFile(filename string, target interface{}) error { + reader, err := newFileReader(filename) + if err != nil { + return err + } + return json.NewDecoder(reader).Decode(target) +} + +func readHTTP(url string, timeout time.Duration, target interface{}) error { + reader, err := newHTTPReader(url, timeout) + if err != nil { + return err + } + return json.NewDecoder(*reader).Decode(target) +} + +// ReadJSON using one of supported transports (file:// http://) +func ReadJSON(uri string, timeout time.Duration, target interface{}) error { + u, err := url.Parse(uri) + if err != nil { + return err + } + if u.Scheme == "file" { + return readFile(u.Path, target) + } + if u.Scheme == "http" || u.Scheme == "https" { + return readHTTP(u.String(), timeout, target) + } + return fmt.Errorf("Unsupported URI scheme '%s' in '%s'", u.Scheme, u) +} diff --git a/transport/transport_test.go b/transport/transport_test.go new file mode 100644 index 000000000..479e9ea27 --- /dev/null +++ b/transport/transport_test.go @@ -0,0 +1,88 @@ +package transport_test + +import ( + "fmt" + "testing" + "time" + + "github.com/cloudflare/unsee/mock" + "github.com/cloudflare/unsee/transport" + + log "github.com/Sirupsen/logrus" + httpmock "gopkg.in/jarcoal/httpmock.v1" +) + +type transportTest struct { + uri string + timeout time.Duration + failed bool +} + +var transportTests = []transportTest{ + transportTest{ + uri: "http://localhost/status", + }, + transportTest{ + uri: "http://localhost/404", + failed: true, + }, + transportTest{ + uri: "http://localhost/invalid", + failed: true, + }, + transportTest{ + uri: "https://localhost/status", + }, + transportTest{ + uri: "https://localhost/404", + failed: true, + }, + transportTest{ + uri: "https://localhost/invalid", + failed: true, + }, + transportTest{ + uri: fmt.Sprintf("file://%s", mock.GetAbsoluteMockPath("status", "0.4")), + }, + transportTest{ + uri: "file:///non-existing-file.abcdef", + failed: true, + }, + transportTest{ + uri: "file://transport.go", + failed: true, + }, +} + +type mockStatus struct { + status string + integer int + yes bool + no bool +} + +func TestFileReader(t *testing.T) { + log.SetLevel(log.ErrorLevel) + 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 { + r := mockStatus{} + err := transport.ReadJSON(testCase.uri, testCase.timeout, &r) + if (err != nil) != testCase.failed { + t.Errorf("[%s] Expected failure: %v, Read() failed: %v, error: %s", testCase.uri, testCase.failed, (err != nil), err) + } + } +} From 5d9ec5da642df82d8d5581de9589df76004d34c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 23:04:58 -0700 Subject: [PATCH 5/7] Use ReadJSON everywhere, remove GetJSONFromURL --- alertmanager/alertmanager_test.go | 13 ++++++--- alertmanager/version.go | 2 +- mapper/v04/alerts.go | 2 +- mapper/v04/silences.go | 2 +- mapper/v05/alerts.go | 2 +- mapper/v05/silences.go | 2 +- transport/http.go | 41 ---------------------------- transport/http_test.go | 44 ------------------------------- 8 files changed, 14 insertions(+), 94 deletions(-) delete mode 100644 transport/http_test.go diff --git a/alertmanager/alertmanager_test.go b/alertmanager/alertmanager_test.go index ad76da2bc..8d385df3e 100644 --- a/alertmanager/alertmanager_test.go +++ b/alertmanager/alertmanager_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/cloudflare/unsee/alertmanager" + "github.com/cloudflare/unsee/config" "github.com/cloudflare/unsee/mock" log "github.com/Sirupsen/logrus" @@ -15,13 +16,15 @@ var testVersions = []string{"0.4", "0.5"} func TestGetAlerts(t *testing.T) { log.SetLevel(log.ErrorLevel) + config.Config.AlertmanagerURI = "http://localhost" + httpmock.Activate() defer httpmock.DeactivateAndReset() for _, version := range testVersions { httpmock.Reset() - mock.RegisterURL("api/v1/status", version, "status") - mock.RegisterURL("api/v1/alerts/groups", version, "alerts/groups") + mock.RegisterURL("http://localhost/api/v1/status", version, "status") + mock.RegisterURL("http://localhost/api/v1/alerts/groups", version, "alerts/groups") v := alertmanager.GetVersion() if !strings.HasPrefix(v, version) { @@ -40,13 +43,15 @@ func TestGetAlerts(t *testing.T) { func TestGetSilences(t *testing.T) { log.SetLevel(log.ErrorLevel) + config.Config.AlertmanagerURI = "http://localhost" + httpmock.Activate() defer httpmock.DeactivateAndReset() for _, version := range testVersions { httpmock.Reset() - mock.RegisterURL("api/v1/status", version, "status") - mock.RegisterURL("api/v1/silences", version, "silences") + mock.RegisterURL("http://localhost/api/v1/status", version, "status") + mock.RegisterURL("http://localhost/api/v1/silences", version, "silences") v := alertmanager.GetVersion() if !strings.HasPrefix(v, version) { diff --git a/alertmanager/version.go b/alertmanager/version.go index c0f3c1501..558d58f49 100644 --- a/alertmanager/version.go +++ b/alertmanager/version.go @@ -29,7 +29,7 @@ func GetVersion() string { return defaultVersion } ver := alertmanagerVersion{} - err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &ver) + err = transport.ReadJSON(url, config.Config.AlertmanagerTimeout, &ver) if err != nil { log.Errorf("%s request failed: %s", url, err.Error()) return defaultVersion diff --git a/mapper/v04/alerts.go b/mapper/v04/alerts.go index edb175c1b..131f35f25 100644 --- a/mapper/v04/alerts.go +++ b/mapper/v04/alerts.go @@ -62,7 +62,7 @@ func (m AlertMapper) GetAlerts() ([]models.AlertGroup, error) { return groups, err } - err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + err = transport.ReadJSON(url, config.Config.AlertmanagerTimeout, &resp) if err != nil { return groups, err } diff --git a/mapper/v04/silences.go b/mapper/v04/silences.go index 416d9ce0a..6d8396b96 100644 --- a/mapper/v04/silences.go +++ b/mapper/v04/silences.go @@ -66,7 +66,7 @@ func (m SilenceMapper) GetSilences() ([]models.Silence, error) { // Alertmanager 0.4 uses pagination for silences url = fmt.Sprintf("%s?limit=%d", url, math.MaxUint32) - err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + err = transport.ReadJSON(url, config.Config.AlertmanagerTimeout, &resp) if err != nil { return silences, err } diff --git a/mapper/v05/alerts.go b/mapper/v05/alerts.go index c1e039363..9308adcde 100644 --- a/mapper/v05/alerts.go +++ b/mapper/v05/alerts.go @@ -60,7 +60,7 @@ func (m AlertMapper) GetAlerts() ([]models.AlertGroup, error) { return groups, err } - err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + err = transport.ReadJSON(url, config.Config.AlertmanagerTimeout, &resp) if err != nil { return groups, err } diff --git a/mapper/v05/silences.go b/mapper/v05/silences.go index 7e56fd9ca..b2855ce14 100644 --- a/mapper/v05/silences.go +++ b/mapper/v05/silences.go @@ -57,7 +57,7 @@ func (m SilenceMapper) GetSilences() ([]models.Silence, error) { return silences, err } - err = transport.GetJSONFromURL(url, config.Config.AlertmanagerTimeout, &resp) + err = transport.ReadJSON(url, config.Config.AlertmanagerTimeout, &resp) if err != nil { return silences, err } diff --git a/transport/http.go b/transport/http.go index b438f0b48..875746ec8 100644 --- a/transport/http.go +++ b/transport/http.go @@ -2,7 +2,6 @@ package transport import ( "compress/gzip" - "encoding/json" "fmt" "io" "net/http" @@ -11,46 +10,6 @@ import ( log "github.com/Sirupsen/logrus" ) -// GetJSONFromURL allows to fetch Alertmanager data over HTTP transport and -// decode it onto provided data structure. -func GetJSONFromURL(url string, timeout time.Duration, target interface{}) error { - log.Infof("GET %s", url) - - c := &http.Client{ - Timeout: timeout, - } - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - req.Header.Add("Accept-Encoding", "gzip") - resp, err := c.Do(req) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Request to Alertmanager failed with %s", resp.Status) - } - - defer resp.Body.Close() - - var reader io.ReadCloser - switch resp.Header.Get("Content-Encoding") { - case "gzip": - reader, err = gzip.NewReader(resp.Body) - if err != nil { - return fmt.Errorf("Failed to decode gzipped content: %s", err.Error()) - } - defer reader.Close() - default: - reader = resp.Body - } - - return json.NewDecoder(reader).Decode(target) -} - type httpReader struct { URL string Timeout time.Duration diff --git a/transport/http_test.go b/transport/http_test.go deleted file mode 100644 index 6a879b7a5..000000000 --- a/transport/http_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package transport_test - -import ( - "testing" - "time" - - "github.com/cloudflare/unsee/transport" - - log "github.com/Sirupsen/logrus" - httpmock "gopkg.in/jarcoal/httpmock.v1" -) - -type mockJSONResponse struct { - status string - integer int - yes bool - no bool -} - -func TestGetJSONFromURL(t *testing.T) { - log.SetLevel(log.ErrorLevel) - httpmock.Activate() - defer httpmock.DeactivateAndReset() - mockJSON := `{ - "response": "success", - "integer": 123, - "yes": true, - "no": false - }` - httpmock.RegisterResponder("GET", "http://localhost/", httpmock.NewStringResponder(200, mockJSON)) - - response := mockJSONResponse{} - err := transport.GetJSONFromURL("http://localhost/", time.Second, &response) - if err != nil { - t.Errorf("getJSONFromURL() failed: %s", err.Error()) - } - - httpmock.RegisterResponder("GET", "http://localhost/404", httpmock.NewStringResponder(404, "Not found")) - response = mockJSONResponse{} - err = transport.GetJSONFromURL("http://localhost/404", time.Second, &response) - if err == nil { - t.Errorf("getJSONFromURL() on invalid url didn't return 404, response: %v", response) - } -} From 7612a8a8aa97c0bc60b55943739ead0816f573b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 23:05:38 -0700 Subject: [PATCH 6/7] Use local files for 'make run' --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e846f7abe..5e1069258 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := unsee VERSION := $(shell git describe --tags --always --dirty='-dev') # Alertmanager instance used when running locally, points to mock data -ALERTMANAGER_URI := https://raw.githubusercontent.com/cloudflare/unsee/master/mock/0.5 +ALERTMANAGER_URI := file://$(CURDIR)mock/0.5 # Listen port when running locally PORT := 8080 From 438c5f946538abd03768bfbd9d9589819f616b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Fri, 14 Apr 2017 23:10:12 -0700 Subject: [PATCH 7/7] Document supported URI schemes --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index fa7b71f91..2433a4448 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,15 @@ silences. Endpoints in use: * ${ALERTMANAGER_URI}/api/v1/alerts/groups * ${ALERTMANAGER_URI}/api/v1/silences +Supported URI schemes: + +* http:// +* https:// +* file:// + +`file://` scheme is only useful for testing purposes, it's used for `make run` +target. + Example: ALERTMANAGER_URI=https://alertmanager.example.com