From 77a92b6e0af62490815c1ca05eb7ae1afe086a47 Mon Sep 17 00:00:00 2001 From: Joao Daher Date: Wed, 20 Sep 2017 14:16:53 -0300 Subject: [PATCH] Add NewRelic deployment hook --- .travis.yml | 1 + README.md | 8 +++- hook/hook.go | 2 + hook/hook_test.go | 11 ++++++ hook/newrelic.go | 74 +++++++++++++++++++++++++++++++++++++ hook/newrelic_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++ testdata/config.yaml | 15 ++++++++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 hook/newrelic.go create mode 100644 hook/newrelic_test.go diff --git a/.travis.yml b/.travis.yml index ef5bc8d..65f8b59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: go install: true go: + - 1.9.x - 1.8.x - 1.7.x - 1.6.x diff --git a/README.md b/README.md index 5c6b755..75bd116 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Here is all avaliables hook's configurations and your descriptions. Remember tha - **webhook_url** Indicates the Webhook URL to dispatch messages to Slack. - Sentry - - **host** Tell to snith your sentry host (e.g http://sentry.io or http://sentry.self.hosted) + - **host** Tell to Snitch your sentry host (e.g http://sentry.io or http://sentry.self.hosted) - **organization_slug** The organization slug is a unique ID used to identify your organization. (You'll find it at your sentry's configuration, probably) - **project_slug** The Project Slug is a unique ID used to identify your project (You'll find it at your project config) - **auth_token** The Auth Token to use the Sentry Web API. You can find more [here](https://docs.sentry.io/api/auth/#auth-tokens) @@ -61,6 +61,12 @@ Here is all avaliables hook's configurations and your descriptions. Remember tha - **access_token** The access token with `post_server_item` scope. You can find more [here](https://rollbar.com/docs/api/#authentication) - **env** The application's environment variable (e.g development, production) +- NewRelic + - **host** Tell to Snitch your NewRelic API host (e.g https://api.newrelic.com) + - **application_id** The application ID is a unique ID used to identify your application in APM. (You'll find it at the end of the application's page URL) + - **api_key** The API Key to use the NewRelic REST API. You can find more [here](https://docs.newrelic.com/docs/apis/rest-api-v2/getting-started/api-keys) + - **revision** The application's current revision (e.g 0.0.1r42) + ## Example [Snitch App Sample](https://github.com/lucasgomide/snitch-app-example) diff --git a/hook/hook.go b/hook/hook.go index 1b1feb1..1965266 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -26,6 +26,8 @@ func Execute(h types.Hook, t types.Tsuru) { h = &Sentry{} case "Rollbar": h = &Rollbar{} + case "Newrelic": + h = &NewRelic{} default: continue } diff --git a/hook/hook_test.go b/hook/hook_test.go index ee0b7c7..bcb4a0a 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -90,6 +90,17 @@ func TestShouldExecuteHooksFromConfig(t *testing.T) { httpmock.RegisterResponder("POST", "http://dummy.sample", httpmock.NewStringResponder(200, `ok`)) + httpmock.RegisterResponder("POST", "https://api.rollbar.com/api/1/deploy/", + httpmock.NewStringResponder(200, `ok`)) + + httpmock.RegisterResponder("POST", "http://sentry.com/api/0/projects/the-answer/for-everything/releases/", + httpmock.NewStringResponder(201, `ok`)) + httpmock.RegisterResponder("POST", "http://sentry.com/api/0/organizations/the-answer/releases/for-everything-v15/deploys/", + httpmock.NewStringResponder(201, `ok`)) + + httpmock.RegisterResponder("POST", "https://api.newrelic.com/v2/applications/01234/deployments.json", + httpmock.NewStringResponder(201, `ok`)) + err = config.ReadConfigFile(configFilePath) if err != nil { t.Error(err) diff --git a/hook/newrelic.go b/hook/newrelic.go new file mode 100644 index 0000000..e2dfec1 --- /dev/null +++ b/hook/newrelic.go @@ -0,0 +1,74 @@ +package hook + +import ( + "bytes" + "errors" + "github.com/lucasgomide/snitch/types" + "net/http" + "time" +) + +type NewRelic struct { + Host string + ApplicationId string + ApiKey string + Revision string +} + +func (s NewRelic) CallHook(deploys []types.Deploy) error { + httpClient := &http.Client{ + Timeout: time.Second * 10, + } + if err := s.createDeploy(httpClient, deploys[0]); err != nil { + return err + } + return nil +} + +func (s *NewRelic) createDeploy(httpClient *http.Client, deploy types.Deploy) error { + data := []byte(` + { + "deployment": { + "revision": "` + s.Revision + `", + "changelog": "", + "description": "", + "user": "` + deploy.User + `" + } + }`) + + url := s.Host+"/v2/applications/"+s.ApplicationId+"/deployments.json" + req, err := http.NewRequest("POST", url, bytes.NewReader(data)) + if err != nil { + return err + } + + req.Header.Add("X-Api-Key", s.ApiKey) + req.Header.Add("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + + if err != nil { + return err + } + if resp.StatusCode != 201 { + return errors.New("NewRelic::CreateDeploy - response status code isn't 201") + } + return nil +} + + +func (s NewRelic) ValidatesFields() error { + if s.Host == "" { + return errors.New("Field host into NewRelic hook is required") + } + if s.ApplicationId == "" { + return errors.New("Field application_id into NewRelic hook is required") + } + if s.ApiKey == "" { + return errors.New("Field api_key into NewRelic hook is required") + } + if s.Revision == "" { + return errors.New("Field revision into NewRelic hook is required") + } + return nil +} diff --git a/hook/newrelic_test.go b/hook/newrelic_test.go new file mode 100644 index 0000000..a9367d5 --- /dev/null +++ b/hook/newrelic_test.go @@ -0,0 +1,85 @@ +package hook + +import ( + "github.com/lucasgomide/snitch/types" + "gopkg.in/jarcoal/httpmock.v1" + "testing" +) + +var newrelic_host = "https://api.newrelic.com" + + +func TestNewRelicDeploySuccessful(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + var deploys []types.Deploy + deploys = append(deploys, types.Deploy{"app-sample", "12345678909", "sha1", "user@g.com", "v15"}) + + s := NewRelic{newrelic_host, "app-id-here", "api-key-here", "revision-here"} + + httpmock.RegisterResponder("POST", s.Host+"/v2/applications/"+s.ApplicationId+"/deployments.json", + httpmock.NewStringResponder(201, `ok`)) + + + if err := s.CallHook(deploys); err != nil { + t.Error(err) + } +} + + +func TestNewRelicReturnsErrorWhenCreateDeployFails(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + var deploys []types.Deploy + deploys = append(deploys, types.Deploy{"app-sample", "12345678909", "sha1", "user@g.com", "v15"}) + + s := NewRelic{newrelic_host, "app-id-here", "api-key-here", "revision-here"} + + httpmock.RegisterResponder("POST", s.Host+"/v2/applications/"+s.ApplicationId+"/deployments.json", + httpmock.NewStringResponder(502, `error`)) + + if err := s.CallHook(deploys); err == nil { + t.Error("Expected returns error, got no error") + } else if err.Error() != "NewRelic::CreateDeploy - response status code isn't 201" { + t.Error(err) + } +} + + +func TestNewRelicValidateFields(t *testing.T) { + s := NewRelic{} + + if err = s.ValidatesFields(); err == nil { + t.Error("Expected returns error, got nil error") + } else if err.Error() != "Field host into NewRelic hook is required" { + t.Error("Expected error Field host into NewRelic hook is required, got", err.Error()) + } + s.Host = "http://abc" + + if err = s.ValidatesFields(); err == nil { + t.Error("Expected returns error, got nil error") + } else if err.Error() != "Field application_id into NewRelic hook is required" { + t.Error("Expected error Field application_id into NewRelic hook is required, got", err.Error()) + } + s.ApplicationId = "app-id-here" + + if err = s.ValidatesFields(); err == nil { + t.Error("Expected returns error, got nil error") + } else if err.Error() != "Field api_key into NewRelic hook is required" { + t.Error("Expected error Field api_key into NewRelic hook is required, got", err.Error()) + } + s.ApiKey = "api-key-here" + + if err = s.ValidatesFields(); err == nil { + t.Error("Expected returns error, got nil error") + } else if err.Error() != "Field revision into NewRelic hook is required" { + t.Error("Expected error Field revision into NewRelic hook is required, got", err.Error()) + } + s.Revision = "revision-here" + + if err = s.ValidatesFields(); err != nil { + t.Error("Expected returns no error, got", err.Error()) + } +} diff --git a/testdata/config.yaml b/testdata/config.yaml index 90d87f0..b9914da 100644 --- a/testdata/config.yaml +++ b/testdata/config.yaml @@ -2,3 +2,18 @@ slack: webhook_url: http://dummy.sample missing_hook: field: value +rollbar: + access_token: abc123 + env: test +sentry: + host: http://sentry.com + organization_slug: the-answer + project_slug: for-everything + auth_token: abc123 + env: test + release_version: 0.0.1 +newrelic: + host: https://api.newrelic.com + application_id: "01234" + api_key: 0a0b11223344 + revision: 0.0.1 \ No newline at end of file