1package pagerduty
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io"
8	"net"
9	"net/http"
10	"runtime"
11	"time"
12)
13
14const (
15	apiEndpoint = "https://api.pagerduty.com"
16)
17
18// APIObject represents generic api json response that is shared by most
19// domain object (like escalation
20type APIObject struct {
21	ID      string `json:"id,omitempty"`
22	Type    string `json:"type,omitempty"`
23	Summary string `json:"summary,omitempty"`
24	Self    string `json:"self,omitempty"`
25	HTMLURL string `json:"html_url,omitempty"`
26}
27
28// APIListObject are the fields used to control pagination when listing objects.
29type APIListObject struct {
30	Limit  uint `url:"limit,omitempty"`
31	Offset uint `url:"offset,omitempty"`
32	More   bool `url:"more,omitempty"`
33	Total  uint `url:"total,omitempty"`
34}
35
36// APIReference are the fields required to reference another API object.
37type APIReference struct {
38	ID   string `json:"id,omitempty"`
39	Type string `json:"type,omitempty"`
40}
41
42type APIDetails struct {
43	Type    string `json:"type,omitempty"`
44	Details string `json:"details,omitempty"`
45}
46
47type errorObject struct {
48	Code    int         `json:"code,omitempty"`
49	Message string      `json:"message,omitempty"`
50	Errors  interface{} `json:"errors,omitempty"`
51}
52
53func newDefaultHTTPClient() *http.Client {
54	return &http.Client{
55		Transport: &http.Transport{
56			Proxy: http.ProxyFromEnvironment,
57			DialContext: (&net.Dialer{
58				Timeout:   30 * time.Second,
59				KeepAlive: 30 * time.Second,
60			}).DialContext,
61			MaxIdleConns:          10,
62			IdleConnTimeout:       60 * time.Second,
63			TLSHandshakeTimeout:   10 * time.Second,
64			ExpectContinueTimeout: 1 * time.Second,
65			MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 1,
66		},
67	}
68}
69
70// HTTPClient is an interface which declares the functionality we need from an
71// HTTP client. This is to allow consumers to provide their own HTTP client as
72// needed, without restricting them to only using *http.Client.
73type HTTPClient interface {
74	Do(*http.Request) (*http.Response, error)
75}
76
77// defaultHTTPClient is our own default HTTP client. We use this, instead of
78// http.DefaultClient, to avoid other packages tweaks to http.DefaultClient
79// causing issues with our HTTP calls. This also allows us to tweak the
80// transport values to be more resilient without making changes to the
81// http.DefaultClient.
82//
83// Keep this unexported so consumers of the package can't make changes to it.
84var defaultHTTPClient HTTPClient = newDefaultHTTPClient()
85
86// Client wraps http client
87type Client struct {
88	authToken string
89
90	// HTTPClient is the HTTP client used for making requests against the
91	// PagerDuty API. You can use either *http.Client here, or your own
92	// implementation.
93	HTTPClient HTTPClient
94}
95
96// NewClient creates an API client
97func NewClient(authToken string) *Client {
98	return &Client{
99		authToken:  authToken,
100		HTTPClient: defaultHTTPClient,
101	}
102}
103
104func (c *Client) delete(path string) (*http.Response, error) {
105	return c.do("DELETE", path, nil, nil)
106}
107
108func (c *Client) put(path string, payload interface{}, headers *map[string]string) (*http.Response, error) {
109
110	if payload != nil {
111		data, err := json.Marshal(payload)
112		if err != nil {
113			return nil, err
114		}
115		return c.do("PUT", path, bytes.NewBuffer(data), headers)
116	}
117	return c.do("PUT", path, nil, headers)
118}
119
120func (c *Client) post(path string, payload interface{}, headers *map[string]string) (*http.Response, error) {
121	data, err := json.Marshal(payload)
122	if err != nil {
123		return nil, err
124	}
125	return c.do("POST", path, bytes.NewBuffer(data), headers)
126}
127
128func (c *Client) get(path string) (*http.Response, error) {
129	return c.do("GET", path, nil, nil)
130}
131
132func (c *Client) do(method, path string, body io.Reader, headers *map[string]string) (*http.Response, error) {
133	endpoint := apiEndpoint + path
134	req, _ := http.NewRequest(method, endpoint, body)
135	req.Header.Set("Accept", "application/vnd.pagerduty+json;version=2")
136	if headers != nil {
137		for k, v := range *headers {
138			req.Header.Set(k, v)
139		}
140	}
141	req.Header.Set("Content-Type", "application/json")
142	req.Header.Set("Authorization", "Token token="+c.authToken)
143
144	resp, err := c.HTTPClient.Do(req)
145	return c.checkResponse(resp, err)
146}
147
148func (c *Client) decodeJSON(resp *http.Response, payload interface{}) error {
149	defer resp.Body.Close()
150	decoder := json.NewDecoder(resp.Body)
151	return decoder.Decode(payload)
152}
153
154func (c *Client) checkResponse(resp *http.Response, err error) (*http.Response, error) {
155	if err != nil {
156		return resp, fmt.Errorf("Error calling the API endpoint: %v", err)
157	}
158	if 199 >= resp.StatusCode || 300 <= resp.StatusCode {
159		var eo *errorObject
160		var getErr error
161		if eo, getErr = c.getErrorFromResponse(resp); getErr != nil {
162			return resp, fmt.Errorf("Response did not contain formatted error: %s. HTTP response code: %v. Raw response: %+v", getErr, resp.StatusCode, resp)
163		}
164		return resp, fmt.Errorf("Failed call API endpoint. HTTP response code: %v. Error: %v", resp.StatusCode, eo)
165	}
166	return resp, nil
167}
168
169func (c *Client) getErrorFromResponse(resp *http.Response) (*errorObject, error) {
170	var result map[string]errorObject
171	if err := c.decodeJSON(resp, &result); err != nil {
172		return nil, fmt.Errorf("Could not decode JSON response: %v", err)
173	}
174	s, ok := result["error"]
175	if !ok {
176		return nil, fmt.Errorf("JSON response does not have error field")
177	}
178	return &s, nil
179}
180