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 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 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/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) 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) +} diff --git a/transport/http.go b/transport/http.go index 15a1bf7c0..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,27 +10,32 @@ 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) +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", url, nil) + req, err := http.NewRequest("GET", hr.URL, nil) if err != nil { - return err + return nil, err } req.Header.Add("Accept-Encoding", "gzip") resp, err := c.Do(req) if err != nil { - return err + return nil, err } if resp.StatusCode != http.StatusOK { - return fmt.Errorf("Request to Alertmanager failed with %s", resp.Status) + return nil, fmt.Errorf("Request to Alertmanager failed with %s", resp.Status) } defer resp.Body.Close() @@ -41,12 +45,11 @@ func GetJSONFromURL(url string, timeout time.Duration, target interface{}) error case "gzip": reader, err = gzip.NewReader(resp.Body) if err != nil { - return fmt.Errorf("Failed to decode gzipped content: %s", err.Error()) + return nil, fmt.Errorf("Failed to decode gzipped content: %s", err.Error()) } defer reader.Close() default: reader = resp.Body } - - return json.NewDecoder(reader).Decode(target) + return &reader, nil } 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) - } -} 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) + } + } +}