github.com/bshelton229/agent@v3.5.4+incompatible/api/buildkite.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "mime/multipart" 10 "net/http" 11 "net/http/httputil" 12 "net/textproto" 13 "net/url" 14 "reflect" 15 "strings" 16 "time" 17 18 "github.com/buildkite/agent/logger" 19 "github.com/google/go-querystring/query" 20 "gopkg.in/vmihailenco/msgpack.v2" 21 ) 22 23 const ( 24 defaultBaseURL = "https://agent.buildkite.com/" 25 defaultUserAgent = "buildkite-agent/api" 26 ) 27 28 // A Client manages communication with the Buildkite Agent API. 29 type Client struct { 30 // HTTP client used to communicate with the API. 31 client *http.Client 32 33 // Base URL for API requests. Defaults to the public Buildkite Agent API. 34 // The URL should always be specified with a trailing slash. 35 BaseURL *url.URL 36 37 // User agent used when communicating with the Buildkite Agent API. 38 UserAgent string 39 40 // If true, requests and responses will be dumped and set to the logger 41 DebugHTTP bool 42 43 // Services used for talking to different parts of the Buildkite Agent API. 44 Agents *AgentsService 45 Pings *PingsService 46 Jobs *JobsService 47 Chunks *ChunksService 48 MetaData *MetaDataService 49 HeaderTimes *HeaderTimesService 50 Artifacts *ArtifactsService 51 Pipelines *PipelinesService 52 Heartbeats *HeartbeatsService 53 Annotations *AnnotationsService 54 } 55 56 // NewClient returns a new Buildkite Agent API Client. 57 func NewClient(httpClient *http.Client) *Client { 58 baseURL, _ := url.Parse(defaultBaseURL) 59 60 c := &Client{ 61 client: httpClient, 62 BaseURL: baseURL, 63 UserAgent: defaultUserAgent, 64 } 65 66 c.Agents = &AgentsService{c} 67 c.Pings = &PingsService{c} 68 c.Jobs = &JobsService{c} 69 c.Chunks = &ChunksService{c} 70 c.MetaData = &MetaDataService{c} 71 c.HeaderTimes = &HeaderTimesService{c} 72 c.Artifacts = &ArtifactsService{c} 73 c.Pipelines = &PipelinesService{c} 74 c.Heartbeats = &HeartbeatsService{c} 75 c.Annotations = &AnnotationsService{c} 76 77 return c 78 } 79 80 // NewRequest creates an API request. A relative URL can be provided in urlStr, 81 // in which case it is resolved relative to the BaseURL of the Client. 82 // Relative URLs should always be specified without a preceding slash. If 83 // specified, the value pointed to by body is JSON encoded and included as the 84 // request body. 85 func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { 86 u := joinURL(c.BaseURL.String(), urlStr) 87 88 buf := new(bytes.Buffer) 89 if body != nil { 90 err := json.NewEncoder(buf).Encode(body) 91 if err != nil { 92 return nil, err 93 } 94 } 95 96 req, err := http.NewRequest(method, u, buf) 97 if err != nil { 98 return nil, err 99 } 100 101 req.Header.Add("User-Agent", c.UserAgent) 102 103 if body != nil { 104 req.Header.Add("Content-Type", "application/json") 105 } 106 107 return req, nil 108 } 109 110 // NewRequestWithMessagePack behaves the same as NewRequest expect it encodes 111 // the body with MessagePack instead of JSON. 112 func (c *Client) NewRequestWithMessagePack(method, urlStr string, body interface{}) (*http.Request, error) { 113 u := joinURL(c.BaseURL.String(), urlStr) 114 115 buf := new(bytes.Buffer) 116 if body != nil { 117 err := msgpack.NewEncoder(buf).Encode(body) 118 if err != nil { 119 return nil, err 120 } 121 } 122 123 req, err := http.NewRequest(method, u, buf) 124 if err != nil { 125 return nil, err 126 } 127 128 req.Header.Add("User-Agent", c.UserAgent) 129 130 if body != nil { 131 req.Header.Add("Content-Type", "application/msgpack") 132 } 133 134 return req, nil 135 } 136 137 // NewFormRequest creates an multi-part form request. A relative URL can be 138 // provided in urlStr, in which case it is resolved relative to the UploadURL 139 // of the Client. Relative URLs should always be specified without a preceding 140 // slash. 141 func (c *Client) NewFormRequest(method, urlStr string, body *bytes.Buffer) (*http.Request, error) { 142 u := joinURL(c.BaseURL.String(), urlStr) 143 144 req, err := http.NewRequest(method, u, body) 145 if err != nil { 146 return nil, err 147 } 148 149 if c.UserAgent != "" { 150 req.Header.Add("User-Agent", c.UserAgent) 151 } 152 153 return req, nil 154 } 155 156 // Response is a Buildkite Agent API response. This wraps the standard 157 // http.Response. 158 type Response struct { 159 *http.Response 160 } 161 162 // newResponse creates a new Response for the provided http.Response. 163 func newResponse(r *http.Response) *Response { 164 response := &Response{Response: r} 165 return response 166 } 167 168 // Do sends an API request and returns the API response. The API response is 169 // JSON decoded and stored in the value pointed to by v, or returned as an 170 // error if an API error has occurred. If v implements the io.Writer 171 // interface, the raw response body will be written to v, without attempting to 172 // first decode it. 173 func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { 174 var err error 175 176 if c.DebugHTTP { 177 // If the request is a multi-part form, then it's probably a 178 // file upload, in which case we don't want to spewing out the 179 // file contents into the debug log (especially if it's been 180 // gzipped) 181 var requestDump []byte 182 if strings.Contains(req.Header.Get("Content-Type"), "multipart/form-data") { 183 requestDump, err = httputil.DumpRequestOut(req, false) 184 } else { 185 requestDump, err = httputil.DumpRequestOut(req, true) 186 } 187 188 logger.Debug("ERR: %s\n%s", err, string(requestDump)) 189 } 190 191 ts := time.Now() 192 193 logger.Debug("%s %s", req.Method, req.URL) 194 195 resp, err := c.client.Do(req) 196 if err != nil { 197 return nil, err 198 } 199 200 logger.Debug("↳ %s %s (%s %s)", req.Method, req.URL, resp.Status, time.Now().Sub(ts)) 201 202 defer resp.Body.Close() 203 defer io.Copy(ioutil.Discard, resp.Body) 204 205 response := newResponse(resp) 206 207 if c.DebugHTTP { 208 responseDump, err := httputil.DumpResponse(resp, true) 209 logger.Debug("\nERR: %s\n%s", err, string(responseDump)) 210 } 211 212 err = checkResponse(resp) 213 if err != nil { 214 // even though there was an error, we still return the response 215 // in case the caller wants to inspect it further 216 return response, err 217 } 218 219 if v != nil { 220 if w, ok := v.(io.Writer); ok { 221 io.Copy(w, resp.Body) 222 } else { 223 if strings.Contains(req.Header.Get("Content-Type"), "application/msgpack") { 224 err = msgpack.NewDecoder(resp.Body).Decode(v) 225 } else { 226 err = json.NewDecoder(resp.Body).Decode(v) 227 } 228 } 229 } 230 231 return response, err 232 } 233 234 // ErrorResponse provides a message. 235 type ErrorResponse struct { 236 Response *http.Response // HTTP response that caused this error 237 Message string `json:"message" msgpack:"message"` // error message 238 } 239 240 func (r *ErrorResponse) Error() string { 241 s := fmt.Sprintf("%v %v: %d", 242 r.Response.Request.Method, r.Response.Request.URL, 243 r.Response.StatusCode) 244 245 if r.Message != "" { 246 s = fmt.Sprintf("%s %v", s, r.Message) 247 } 248 249 return s 250 } 251 252 func checkResponse(r *http.Response) error { 253 if c := r.StatusCode; 200 <= c && c <= 299 { 254 return nil 255 } 256 257 errorResponse := &ErrorResponse{Response: r} 258 data, err := ioutil.ReadAll(r.Body) 259 if err == nil && data != nil { 260 if strings.Contains(r.Header.Get("Content-Type"), "application/msgpack") { 261 msgpack.Unmarshal(data, errorResponse) 262 } else { 263 json.Unmarshal(data, errorResponse) 264 } 265 } 266 267 return errorResponse 268 } 269 270 // addOptions adds the parameters in opt as URL query parameters to s. opt must 271 // be a struct whose fields may contain "url" tags. 272 func addOptions(s string, opt interface{}) (string, error) { 273 v := reflect.ValueOf(opt) 274 if v.Kind() == reflect.Ptr && v.IsNil() { 275 return s, nil 276 } 277 278 u, err := url.Parse(s) 279 if err != nil { 280 return s, err 281 } 282 283 qs, err := query.Values(opt) 284 if err != nil { 285 return s, err 286 } 287 288 u.RawQuery = qs.Encode() 289 return u.String(), nil 290 } 291 292 // Copied from http://golang.org/src/mime/multipart/writer.go 293 var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 294 295 func escapeQuotes(s string) string { 296 return quoteEscaper.Replace(s) 297 } 298 299 // createFormFileWithContentType is a copy of the CreateFormFile method, except 300 // you can change the content type it uses (by default you can't) 301 func createFormFileWithContentType(w *multipart.Writer, fieldname, filename, contentType string) (io.Writer, error) { 302 h := make(textproto.MIMEHeader) 303 h.Set("Content-Disposition", 304 fmt.Sprintf(`form-data; name="%s"; filename="%s"`, 305 escapeQuotes(fieldname), escapeQuotes(filename))) 306 h.Set("Content-Type", contentType) 307 return w.CreatePart(h) 308 } 309 310 func joinURL(endpoint string, path string) string { 311 return strings.TrimRight(endpoint, "/") + "/" + strings.TrimLeft(path, "/") 312 }