From 0650d1c8cc75fdb3c4c604cbfffd18601935b45f Mon Sep 17 00:00:00 2001 From: Stevenson Jean-Pierre Date: Fri, 9 Oct 2020 19:19:44 -0400 Subject: [PATCH 1/4] Adds changes events to client --- change_events.go | 75 +++++++++++++++++++++++++++++++++++ change_events_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 change_events.go create mode 100644 change_events_test.go diff --git a/change_events.go b/change_events.go new file mode 100644 index 00000000..8de953a6 --- /dev/null +++ b/change_events.go @@ -0,0 +1,75 @@ +package pagerduty + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" +) + +const EventsEndPoint = "https://events.pagerduty.com" +const ChangeEventPath = "v2/change/enqueue" + +type ChangeEvent struct { + RoutingKey string `json:"routing_key"` + Payload Payload `json:"payload"` + Links []Link `json:"links"` +} + +type Payload struct { + Source string `json:"source"` + Summary string `json:"summary"` + Timestamp time.Time `json:"time"` + TimestampString string `json:"timestamp"` + CustomDetails map[string]string `json:"custom_details"` +} + +type Link struct { + Href string `json:"href"` + Text string `json:"text"` +} + +// Response is the json response body for an event +type ChangeEventResponse struct { + Status string `json:"status,omitempty"` + Message string `json:"message,omitempty"` + Errors []string `json:"errors,omitempty"` +} + +// ManageEvent handles the trigger, acknowledge, and resolve methods for an event +func (c *Client) SendChangeEvent(e ChangeEvent) (*ChangeEventResponse, error) { + //PagerDuty expects RFC3339 formatted timestamp so we do the conversion here + e.Payload.TimestampString = e.Payload.Timestamp.Format(time.RFC3339) + data, err := json.Marshal(e) + if err != nil { + return nil, err + } + //Allows custom endpoints to be passed in from the http client for testing and future customer needs + endPoint := strings.Join([]string{EventsEndPoint, ChangeEventPath}, "/") + if c.apiEndpoint != "https://api.pagerduty.com" { + endPoint = strings.Join([]string{c.apiEndpoint, ChangeEventPath}, "/") + } + req, _ := http.NewRequest("POST", endPoint, bytes.NewBuffer(data)) + req.Header.Set("User-Agent", "go-pagerduty/"+Version) + req.Header.Set("Content-Type", "application/json") + resp, err := c.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("HTTP Status Code: %d", resp.StatusCode) + } + return nil, fmt.Errorf("HTTP Status Code: %d, Message: %s", resp.StatusCode, string(b)) + } + var eventResponse ChangeEventResponse + if err := json.NewDecoder(resp.Body).Decode(&eventResponse); err != nil { + return nil, err + } + return &eventResponse, nil +} diff --git a/change_events_test.go b/change_events_test.go new file mode 100644 index 00000000..041c570f --- /dev/null +++ b/change_events_test.go @@ -0,0 +1,91 @@ +package pagerduty + +import ( + "io/ioutil" + "net/http" + "testing" + "time" +) + +var reqBody = `{"routing_key":"a0000000aa0000a0a000aa0a0a0aa000","payload":{"source":"Test runner",` + + `"summary":"Summary can't be blank","time":"2020-10-09T18:53:40.635779-04:00",` + + `"timestamp":"2020-10-09T18:53:40-04:00","custom_details":{"DetailKey1":"DetailValue1",` + + `"DetailKey2":"DetailValue2"}},"links":[{"href":"https://acme.pagerduty.dev/build/2",` + + `"text":"View more details in Acme!"},{"href":"https://acme2.pagerduty.dev/build/2",` + + `"text":"View more details in Acme2!"}]}` + +func TestClient_SendChangeEvent(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(`{"message": "Change event processed", "status": "success"}`)) + }) + + var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient} + + want := ChangeEventResponse{ + Status: "success", + Message: "Change event processed", + } + + eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} + ce := ChangeEvent{ + RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", + Payload: Payload{Source: "Test runner", Summary: "Summary can't be blank", Timestamp: time.Now(), CustomDetails: eventDetails}, + Links: []Link{ + { + Href: "https://acme.pagerduty.dev/build/2", + Text: "View more details in Acme!", + }, + { + Href: "https://acme2.pagerduty.dev/build/2", + Text: "View more details in Acme2!", + }}, + } + + res, err := client.SendChangeEvent(ce) + + if err != nil { + t.Fatal(err) + } + testEqual(t, want, *res) +} + +func TestClient_SendChangeEventContent(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + testEqual(t, reqBody, string(body)) + }) + + var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient} + + eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} + ts, _ := time.Parse(time.RFC3339, "2020-10-09T18:53:40.635779-04:00") + ce := ChangeEvent{ + RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", + Payload: Payload{Source: "Test runner", Summary: "Summary can't be blank", + Timestamp: ts, CustomDetails: eventDetails}, + Links: []Link{ + { + Href: "https://acme.pagerduty.dev/build/2", + Text: "View more details in Acme!", + }, + { + Href: "https://acme2.pagerduty.dev/build/2", + Text: "View more details in Acme2!", + }}, + } + + _, _ = client.SendChangeEvent(ce) + +} From ee63a2046a4d373cbe27dab65cec25c4770560ec Mon Sep 17 00:00:00 2001 From: Stevenson Jean-Pierre Date: Sun, 18 Oct 2020 23:38:13 -0400 Subject: [PATCH 2/4] Refactored to match PR #241 style --- change_events.go | 68 ++++++++++++++++----------------- change_events_test.go | 88 +++++++++++++++++++++++++++---------------- 2 files changed, 90 insertions(+), 66 deletions(-) diff --git a/change_events.go b/change_events.go index 8de953a6..677b2dc4 100644 --- a/change_events.go +++ b/change_events.go @@ -3,73 +3,73 @@ package pagerduty import ( "bytes" "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "time" + "errors" ) -const EventsEndPoint = "https://events.pagerduty.com" -const ChangeEventPath = "v2/change/enqueue" +const ChangeEventPath = "/v2/change/enqueue" +// ChangeEvent represents a ChangeEvent's request parameters +// https://developer.pagerduty.com/docs/events-api-v2/send-change-events/#parameters type ChangeEvent struct { RoutingKey string `json:"routing_key"` Payload Payload `json:"payload"` Links []Link `json:"links"` } +// Payload ChangeEvent Payload +// https://developer.pagerduty.com/docs/events-api-v2/send-change-events/#example-request-payload type Payload struct { - Source string `json:"source"` - Summary string `json:"summary"` - Timestamp time.Time `json:"time"` - TimestampString string `json:"timestamp"` - CustomDetails map[string]string `json:"custom_details"` + Source string `json:"source"` + Summary string `json:"summary"` + Timestamp string `json:"timestamp"` + CustomDetails map[string]string `json:"custom_details"` } +// Link represents a single link in a ChangeEvent +// https://developer.pagerduty.com/docs/events-api-v2/send-change-events/#the-links-property type Link struct { Href string `json:"href"` Text string `json:"text"` } -// Response is the json response body for an event +// ChangeEventResponse is the json response body for an event type ChangeEventResponse struct { Status string `json:"status,omitempty"` Message string `json:"message,omitempty"` Errors []string `json:"errors,omitempty"` } -// ManageEvent handles the trigger, acknowledge, and resolve methods for an event -func (c *Client) SendChangeEvent(e ChangeEvent) (*ChangeEventResponse, error) { - //PagerDuty expects RFC3339 formatted timestamp so we do the conversion here - e.Payload.TimestampString = e.Payload.Timestamp.Format(time.RFC3339) +// CreateChangeEvent Sends PagerDuty a single ChangeEvent to record +// The v2EventsAPIEndpoint parameter must be set on the client +// Documentation can be found at https://developer.pagerduty.com/docs/events-api-v2/send-change-events +func (c *Client) CreateChangeEvent(e ChangeEvent) (*ChangeEventResponse, error) { + if c.v2EventsAPIEndpoint == "" { + return nil, errors.New("v2EventsAPIEndpoint field must be set on Client") + } + + headers := make(map[string]string) + data, err := json.Marshal(e) if err != nil { return nil, err } - //Allows custom endpoints to be passed in from the http client for testing and future customer needs - endPoint := strings.Join([]string{EventsEndPoint, ChangeEventPath}, "/") - if c.apiEndpoint != "https://api.pagerduty.com" { - endPoint = strings.Join([]string{c.apiEndpoint, ChangeEventPath}, "/") - } - req, _ := http.NewRequest("POST", endPoint, bytes.NewBuffer(data)) - req.Header.Set("User-Agent", "go-pagerduty/"+Version) - req.Header.Set("Content-Type", "application/json") - resp, err := c.HTTPClient.Do(req) + + resp, err := c.doWithEndpoint( + c.v2EventsAPIEndpoint, + "POST", + ChangeEventPath, + false, + bytes.NewBuffer(data), + &headers, + ) if err != nil { return nil, err } - defer resp.Body.Close() - if resp.StatusCode != http.StatusAccepted { - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("HTTP Status Code: %d", resp.StatusCode) - } - return nil, fmt.Errorf("HTTP Status Code: %d, Message: %s", resp.StatusCode, string(b)) - } + var eventResponse ChangeEventResponse if err := json.NewDecoder(resp.Body).Decode(&eventResponse); err != nil { return nil, err } + return &eventResponse, nil } diff --git a/change_events_test.go b/change_events_test.go index 041c570f..3abf84e8 100644 --- a/change_events_test.go +++ b/change_events_test.go @@ -4,27 +4,34 @@ import ( "io/ioutil" "net/http" "testing" - "time" ) -var reqBody = `{"routing_key":"a0000000aa0000a0a000aa0a0a0aa000","payload":{"source":"Test runner",` + - `"summary":"Summary can't be blank","time":"2020-10-09T18:53:40.635779-04:00",` + - `"timestamp":"2020-10-09T18:53:40-04:00","custom_details":{"DetailKey1":"DetailValue1",` + - `"DetailKey2":"DetailValue2"}},"links":[{"href":"https://acme.pagerduty.dev/build/2",` + - `"text":"View more details in Acme!"},{"href":"https://acme2.pagerduty.dev/build/2",` + - `"text":"View more details in Acme2!"}]}` +const ( + expectedChangeCreatePayload = `{"routing_key":"a0000000aa0000a0a000aa0a0a0aa000","payload":{"source":"Test runner",` + + `"summary":"Summary can't be blank","timestamp":"2020-10-19T03:06:16.318Z",` + + `"custom_details":{"DetailKey1":"DetailValue1","DetailKey2":"DetailValue2"}},` + + `"links":[{"href":"https://acme.pagerduty.dev/build/2","text":"View more details in Acme!"},` + + `{"href":"https://acme2.pagerduty.dev/build/2","text":"View more details in Acme2!"}]}` +) -func TestClient_SendChangeEvent(t *testing.T) { +func TestChangeEvent_Create(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "POST") - w.WriteHeader(http.StatusAccepted) - w.Write([]byte(`{"message": "Change event processed", "status": "success"}`)) - }) + mux.HandleFunc( + "/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusAccepted) + w.Write([]byte(`{"message": "Change event processed", "status": "success"}`)) + }, + ) - var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient} + var client = &Client{ + v2EventsAPIEndpoint: server.URL, + apiEndpoint: server.URL, + authToken: "foo", + HTTPClient: defaultHTTPClient, + } want := ChangeEventResponse{ Status: "success", @@ -34,7 +41,12 @@ func TestClient_SendChangeEvent(t *testing.T) { eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} ce := ChangeEvent{ RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", - Payload: Payload{Source: "Test runner", Summary: "Summary can't be blank", Timestamp: time.Now(), CustomDetails: eventDetails}, + Payload: Payload{ + Source: "Test runner", + Summary: "Summary can't be blank", + Timestamp: "2020-10-19T03:06:16.318Z", + CustomDetails: eventDetails, + }, Links: []Link{ { Href: "https://acme.pagerduty.dev/build/2", @@ -43,10 +55,11 @@ func TestClient_SendChangeEvent(t *testing.T) { { Href: "https://acme2.pagerduty.dev/build/2", Text: "View more details in Acme2!", - }}, + }, + }, } - res, err := client.SendChangeEvent(ce) + res, err := client.CreateChangeEvent(ce) if err != nil { t.Fatal(err) @@ -54,27 +67,37 @@ func TestClient_SendChangeEvent(t *testing.T) { testEqual(t, want, *res) } -func TestClient_SendChangeEventContent(t *testing.T) { +func TestChangeEvent_CreateWithPayloadVerification(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "POST") - body, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Fatal(err) - } - testEqual(t, reqBody, string(body)) - }) + mux.HandleFunc( + "/v2/change/enqueue", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + testEqual(t, expectedChangeCreatePayload, string(body)) + }, + ) - var client = &Client{apiEndpoint: server.URL, authToken: "foo", HTTPClient: defaultHTTPClient} + var client = &Client{ + v2EventsAPIEndpoint: server.URL, + apiEndpoint: server.URL, + authToken: "foo", + HTTPClient: defaultHTTPClient, + } eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} - ts, _ := time.Parse(time.RFC3339, "2020-10-09T18:53:40.635779-04:00") ce := ChangeEvent{ RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", - Payload: Payload{Source: "Test runner", Summary: "Summary can't be blank", - Timestamp: ts, CustomDetails: eventDetails}, + Payload: Payload{ + Source: "Test runner", + Summary: "Summary can't be blank", + Timestamp: "2020-10-19T03:06:16.318Z", + CustomDetails: eventDetails, + }, Links: []Link{ { Href: "https://acme.pagerduty.dev/build/2", @@ -83,9 +106,10 @@ func TestClient_SendChangeEventContent(t *testing.T) { { Href: "https://acme2.pagerduty.dev/build/2", Text: "View more details in Acme2!", - }}, + }, + }, } - _, _ = client.SendChangeEvent(ce) + _, _ = client.CreateChangeEvent(ce) } From 8ed5cfbd0df33876482c61b4335dd189572e93ea Mon Sep 17 00:00:00 2001 From: Stevenson Jean-Pierre Date: Mon, 19 Oct 2020 16:39:29 -0400 Subject: [PATCH 3/4] Changed custom details field to interface{} --- change_events.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/change_events.go b/change_events.go index 677b2dc4..c1f9308f 100644 --- a/change_events.go +++ b/change_events.go @@ -19,10 +19,10 @@ type ChangeEvent struct { // Payload ChangeEvent Payload // https://developer.pagerduty.com/docs/events-api-v2/send-change-events/#example-request-payload type Payload struct { - Source string `json:"source"` - Summary string `json:"summary"` - Timestamp string `json:"timestamp"` - CustomDetails map[string]string `json:"custom_details"` + Source string `json:"source"` + Summary string `json:"summary"` + Timestamp string `json:"timestamp"` + CustomDetails interface{} `json:"custom_details"` } // Link represents a single link in a ChangeEvent From 43b87704c4d73ef32471a50e1105771c19b02d4f Mon Sep 17 00:00:00 2001 From: Stevenson Jean-Pierre Date: Mon, 19 Oct 2020 16:47:11 -0400 Subject: [PATCH 4/4] string key interface values --- change_events.go | 8 ++++---- change_events_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/change_events.go b/change_events.go index c1f9308f..08a63f82 100644 --- a/change_events.go +++ b/change_events.go @@ -19,10 +19,10 @@ type ChangeEvent struct { // Payload ChangeEvent Payload // https://developer.pagerduty.com/docs/events-api-v2/send-change-events/#example-request-payload type Payload struct { - Source string `json:"source"` - Summary string `json:"summary"` - Timestamp string `json:"timestamp"` - CustomDetails interface{} `json:"custom_details"` + Source string `json:"source"` + Summary string `json:"summary"` + Timestamp string `json:"timestamp"` + CustomDetails map[string]interface{} `json:"custom_details"` } // Link represents a single link in a ChangeEvent diff --git a/change_events_test.go b/change_events_test.go index 3abf84e8..46b3e490 100644 --- a/change_events_test.go +++ b/change_events_test.go @@ -38,7 +38,7 @@ func TestChangeEvent_Create(t *testing.T) { Message: "Change event processed", } - eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} + eventDetails := map[string]interface{}{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} ce := ChangeEvent{ RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", Payload: Payload{ @@ -89,7 +89,7 @@ func TestChangeEvent_CreateWithPayloadVerification(t *testing.T) { HTTPClient: defaultHTTPClient, } - eventDetails := map[string]string{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} + eventDetails := map[string]interface{}{"DetailKey1": "DetailValue1", "DetailKey2": "DetailValue2"} ce := ChangeEvent{ RoutingKey: "a0000000aa0000a0a000aa0a0a0aa000", Payload: Payload{