blob: 1e4ab0673c6a362fbbce33cd3532232dbc8dbe59 [file] [log] [blame]
Ash Wilson89466cc2014-08-29 11:27:39 -04001package gophercloud
2
Ash Wilson89eec332015-02-12 13:40:32 -05003import (
4 "bytes"
5 "encoding/json"
Jon Perritte0f9e4f2016-02-21 21:41:03 -06006 "errors"
Ash Wilson89eec332015-02-12 13:40:32 -05007 "fmt"
8 "io"
9 "io/ioutil"
10 "net/http"
Jon Perritte0f9e4f2016-02-21 21:41:03 -060011 "runtime"
Jon Perritt2b5e3e12015-02-13 12:15:08 -070012 "strings"
Ash Wilson89eec332015-02-12 13:40:32 -050013)
14
Jon Perritt2b5e3e12015-02-13 12:15:08 -070015// DefaultUserAgent is the default User-Agent string set in the request header.
jrperrittb1013232016-02-10 19:01:53 -060016const DefaultUserAgent = "gophercloud/2.0.0"
Jon Perritt2b5e3e12015-02-13 12:15:08 -070017
18// UserAgent represents a User-Agent header.
19type UserAgent struct {
20 // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
21 // All the strings to prepend are accumulated and prepended in the Join method.
22 prepend []string
23}
24
25// Prepend prepends a user-defined string to the default User-Agent string. Users
26// may pass in one or more strings to prepend.
27func (ua *UserAgent) Prepend(s ...string) {
28 ua.prepend = append(s, ua.prepend...)
29}
30
31// Join concatenates all the user-defined User-Agend strings with the default
32// Gophercloud User-Agent string.
33func (ua *UserAgent) Join() string {
34 uaSlice := append(ua.prepend, DefaultUserAgent)
35 return strings.Join(uaSlice, " ")
36}
37
Jamie Hannafordb280dea2014-10-24 15:14:06 +020038// ProviderClient stores details that are required to interact with any
39// services within a specific provider's API.
Ash Wilson89466cc2014-08-29 11:27:39 -040040//
Jamie Hannafordb280dea2014-10-24 15:14:06 +020041// Generally, you acquire a ProviderClient by calling the NewClient method in
42// the appropriate provider's child package, providing whatever authentication
43// credentials are required.
Ash Wilson89466cc2014-08-29 11:27:39 -040044type ProviderClient struct {
Jamie Hannafordb280dea2014-10-24 15:14:06 +020045 // IdentityBase is the base URL used for a particular provider's identity
46 // service - it will be used when issuing authenticatation requests. It
47 // should point to the root resource of the identity service, not a specific
48 // identity version.
Ash Wilson09694b92014-09-09 14:08:27 -040049 IdentityBase string
50
Jamie Hannafordb280dea2014-10-24 15:14:06 +020051 // IdentityEndpoint is the identity endpoint. This may be a specific version
52 // of the identity service. If this is the case, this endpoint is used rather
53 // than querying versions first.
Ash Wilsonc6372fe2014-09-03 11:24:52 -040054 IdentityEndpoint string
55
Jamie Hannafordb280dea2014-10-24 15:14:06 +020056 // TokenID is the ID of the most recently issued valid token.
Ash Wilson89466cc2014-08-29 11:27:39 -040057 TokenID string
Ash Wilsonb8401a72014-09-08 17:07:49 -040058
Jamie Hannafordb280dea2014-10-24 15:14:06 +020059 // EndpointLocator describes how this provider discovers the endpoints for
60 // its constituent services.
Ash Wilsonb8401a72014-09-08 17:07:49 -040061 EndpointLocator EndpointLocator
Ash Wilson89eec332015-02-12 13:40:32 -050062
63 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
64 HTTPClient http.Client
Jon Perritt2b5e3e12015-02-13 12:15:08 -070065
66 // UserAgent represents the User-Agent header in the HTTP request.
67 UserAgent UserAgent
Jon Perrittf4052c62015-02-14 09:48:18 -070068
Jon Perrittf4052c62015-02-14 09:48:18 -070069 // ReauthFunc is the function used to re-authenticate the user if the request
70 // fails with a 401 HTTP response code. This a needed because there may be multiple
71 // authentication functions for different Identity service versions.
Jon Perritt6fe7c402015-02-17 12:24:53 -070072 ReauthFunc func() error
Ash Wilson89466cc2014-08-29 11:27:39 -040073}
74
Jamie Hannafordb280dea2014-10-24 15:14:06 +020075// AuthenticatedHeaders returns a map of HTTP headers that are common for all
76// authenticated service requests.
Ash Wilson89466cc2014-08-29 11:27:39 -040077func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
Ash Wilson89eec332015-02-12 13:40:32 -050078 if client.TokenID == "" {
79 return map[string]string{}
80 }
Ash Wilson89466cc2014-08-29 11:27:39 -040081 return map[string]string{"X-Auth-Token": client.TokenID}
82}
Ash Wilson89eec332015-02-12 13:40:32 -050083
84// RequestOpts customizes the behavior of the provider.Request() method.
85type RequestOpts struct {
86 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
87 // content type of the request will default to "application/json" unless overridden by MoreHeaders.
88 // It's an error to specify both a JSONBody and a RawBody.
89 JSONBody interface{}
jrperrittb1013232016-02-10 19:01:53 -060090 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
Ash Wilson89eec332015-02-12 13:40:32 -050091 // will be set unless one is provided explicitly by MoreHeaders.
jrperrittb1013232016-02-10 19:01:53 -060092 RawBody io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -050093 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
94 // JSON.
Ash Wilson2491b4c2015-02-12 16:13:39 -050095 JSONResponse interface{}
Ash Wilson89eec332015-02-12 13:40:32 -050096 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
97 // the response has a different code, an error will be returned.
98 OkCodes []int
Ash Wilson89eec332015-02-12 13:40:32 -050099 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
100 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
101 // the default Accept header or an inferred Content-Type, for example.
102 MoreHeaders map[string]string
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600103 // ErrorType specifies the resource error type to return if an error is encountered.
104 // This lets resources override default error messages based on the response status code.
105 ErrorContext error
Ash Wilson89eec332015-02-12 13:40:32 -0500106}
107
Ash Wilson89eec332015-02-12 13:40:32 -0500108var applicationJSON = "application/json"
109
110// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
111// header will automatically be provided.
112func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
jrperrittb1013232016-02-10 19:01:53 -0600113 var body io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -0500114 var contentType *string
115
116 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
Brendan ODonnella69b3472015-04-27 13:59:41 -0500117 // io.ReadSeeker as-is. Default the content-type to application/json.
Ash Wilson89eec332015-02-12 13:40:32 -0500118 if options.JSONBody != nil {
119 if options.RawBody != nil {
120 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
121 }
122
123 rendered, err := json.Marshal(options.JSONBody)
124 if err != nil {
125 return nil, err
126 }
127
128 body = bytes.NewReader(rendered)
129 contentType = &applicationJSON
130 }
131
132 if options.RawBody != nil {
133 body = options.RawBody
134 }
135
136 // Construct the http.Request.
Ash Wilson89eec332015-02-12 13:40:32 -0500137 req, err := http.NewRequest(method, url, body)
138 if err != nil {
139 return nil, err
140 }
141
142 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
143 // modify or omit any header.
Ash Wilson89eec332015-02-12 13:40:32 -0500144 if contentType != nil {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500145 req.Header.Set("Content-Type", *contentType)
Ash Wilson89eec332015-02-12 13:40:32 -0500146 }
Ash Wilson54d62fa2015-02-12 15:09:46 -0500147 req.Header.Set("Accept", applicationJSON)
Ash Wilson89eec332015-02-12 13:40:32 -0500148
149 for k, v := range client.AuthenticatedHeaders() {
150 req.Header.Add(k, v)
151 }
152
Jon Perrittf0a1fee2015-02-13 12:53:23 -0700153 // Set the User-Agent header
154 req.Header.Set("User-Agent", client.UserAgent.Join())
155
Ash Wilson89eec332015-02-12 13:40:32 -0500156 if options.MoreHeaders != nil {
157 for k, v := range options.MoreHeaders {
158 if v != "" {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500159 req.Header.Set(k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500160 } else {
161 req.Header.Del(k)
162 }
163 }
164 }
165
Kostiantyn Yarovyi3fa30bb2015-11-25 17:21:03 +0200166 // Set connection parameter to close the connection immediately when we've got the response
167 req.Close = true
Jon Perrittaaafa612016-02-21 18:23:38 -0600168
Jon Perritt2b5e3e12015-02-13 12:15:08 -0700169 // Issue the request.
Ash Wilson89eec332015-02-12 13:40:32 -0500170 resp, err := client.HTTPClient.Do(req)
171 if err != nil {
172 return nil, err
173 }
174
Jon Perritt6fe7c402015-02-17 12:24:53 -0700175 if resp.StatusCode == http.StatusUnauthorized {
176 if client.ReauthFunc != nil {
177 err = client.ReauthFunc()
Jon Perrittf4052c62015-02-14 09:48:18 -0700178 if err != nil {
179 return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
180 }
jrperrittb1013232016-02-10 19:01:53 -0600181 if seeker, ok := options.RawBody.(io.ReadSeeker); ok && options.RawBody != nil {
182 seeker.Seek(0, 0)
Jon Perrittfcedd7b2015-06-15 19:41:01 -0600183 }
Fredi Pevcina979be92015-10-20 09:13:29 +0200184 resp.Body.Close()
Jon Perrittf4052c62015-02-14 09:48:18 -0700185 resp, err = client.Request(method, url, options)
186 if err != nil {
187 return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
188 }
Clinton Kitsonfcd283a2016-01-07 09:00:56 -0800189
190 return resp, nil
Jon Perrittf4052c62015-02-14 09:48:18 -0700191 }
192 }
193
Jamie Hannaford647cea52015-03-23 17:15:07 +0100194 // Allow default OkCodes if none explicitly set
195 if options.OkCodes == nil {
196 options.OkCodes = defaultOkCodes(method)
197 }
198
199 // Validate the HTTP response status.
200 var ok bool
201 for _, code := range options.OkCodes {
202 if resp.StatusCode == code {
203 ok = true
204 break
Ash Wilson89eec332015-02-12 13:40:32 -0500205 }
Jamie Hannaford647cea52015-03-23 17:15:07 +0100206 }
207 if !ok {
208 body, _ := ioutil.ReadAll(resp.Body)
209 resp.Body.Close()
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600210 pc := make([]uintptr, 1) // at least 1 entry needed
211 runtime.Callers(2, pc)
212 f := runtime.FuncForPC(pc[0])
213 respErr := &ErrUnexpectedResponseCode{
214 BaseError: &BaseError{
215 Function: f.Name(),
216 },
Jamie Hannaford647cea52015-03-23 17:15:07 +0100217 URL: url,
218 Method: method,
219 Expected: options.OkCodes,
220 Actual: resp.StatusCode,
221 Body: body,
Ash Wilson89eec332015-02-12 13:40:32 -0500222 }
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600223
224 errType := options.ErrorContext
225 switch resp.StatusCode {
226 case http.StatusBadRequest:
227 err = ErrDefault400{respErr}
228 if error400er, ok := errType.(Err400er); ok {
229 err = error400er.Error400(respErr)
230 }
231 case http.StatusUnauthorized:
232 if client.ReauthFunc != nil {
233 err = client.ReauthFunc()
234 if err != nil {
235 return nil, &ErrUnableToReauthenticate{
236 &BaseError{
237 OriginalError: respErr,
238 },
239 }
240 }
241 if options.RawBody != nil {
242 seeker, ok := options.RawBody.(io.Seeker)
243 if !ok {
244 return nil, &ErrErrorAfterReauthentication{
245 &BaseError{
246 OriginalError: errors.New("Couldn't seek to beginning of content."),
247 },
248 }
249 }
250 seeker.Seek(0, 0)
251 }
252 resp, err = client.Request(method, url, options)
253 if err != nil {
254 switch err.(type) {
255 case *ErrUnexpectedResponseCode:
256 return nil, &ErrErrorAfterReauthentication{&BaseError{OriginalError: err.(*ErrUnexpectedResponseCode)}}
257 default:
258 return nil, &ErrErrorAfterReauthentication{
259 &BaseError{
260 OriginalError: err,
261 },
262 }
263 }
264 }
265 return resp, nil
266 }
267 err = ErrDefault401{respErr}
268 if error401er, ok := errType.(Err401er); ok {
269 err = error401er.Error401(respErr)
270 }
271 case http.StatusNotFound:
272 err = ErrDefault404{respErr}
273 if error404er, ok := errType.(Err404er); ok {
274 err = error404er.Error404(respErr)
275 }
276 case http.StatusMethodNotAllowed:
277 err = ErrDefault405{respErr}
278 if error405er, ok := errType.(Err405er); ok {
279 err = error405er.Error405(respErr)
280 }
281 case http.StatusRequestTimeout:
282 err = ErrDefault408{respErr}
283 if error408er, ok := errType.(Err408er); ok {
284 err = error408er.Error408(respErr)
285 }
286 case 429:
287 err = ErrDefault429{respErr}
288 if error429er, ok := errType.(Err429er); ok {
289 err = error429er.Error429(respErr)
290 }
291 case http.StatusInternalServerError:
292 err = ErrDefault500{respErr}
293 if error500er, ok := errType.(Err500er); ok {
294 err = error500er.Error500(respErr)
295 }
296 case http.StatusServiceUnavailable:
297 err = ErrDefault503{respErr}
298 if error503er, ok := errType.(Err503er); ok {
299 err = error503er.Error503(respErr)
300 }
301 }
302
303 if err == nil {
304 err = respErr
305 }
306
307 return resp, err
Ash Wilson89eec332015-02-12 13:40:32 -0500308 }
309
310 // Parse the response body as JSON, if requested to do so.
Ash Wilson89eec332015-02-12 13:40:32 -0500311 if options.JSONResponse != nil {
312 defer resp.Body.Close()
Pratik Mallyaee675fd2015-09-14 14:07:30 -0500313 if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
314 return nil, err
315 }
Ash Wilson89eec332015-02-12 13:40:32 -0500316 }
317
318 return resp, nil
319}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100320
321func defaultOkCodes(method string) []int {
322 switch {
323 case method == "GET":
324 return []int{200}
325 case method == "POST":
326 return []int{201, 202}
327 case method == "PUT":
328 return []int{201, 202}
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100329 case method == "PATCH":
330 return []int{200, 204}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100331 case method == "DELETE":
332 return []int{202, 204}
333 }
334
335 return []int{}
336}
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100337
Jon Perrittaaafa612016-02-21 18:23:38 -0600338// Get calls `Request` with the "GET" HTTP verb.
339func (client *ProviderClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100340 if opts == nil {
341 opts = &RequestOpts{}
342 }
343 if JSONResponse != nil {
344 opts.JSONResponse = JSONResponse
345 }
346 return client.Request("GET", url, *opts)
347}
348
Jon Perrittaaafa612016-02-21 18:23:38 -0600349// Post calls `Request` with the "POST" HTTP verb.
350func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100351 if opts == nil {
352 opts = &RequestOpts{}
353 }
354
Brendan ODonnella69b3472015-04-27 13:59:41 -0500355 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100356 opts.RawBody = v
357 } else if JSONBody != nil {
358 opts.JSONBody = JSONBody
359 }
360
361 if JSONResponse != nil {
362 opts.JSONResponse = JSONResponse
363 }
364
365 return client.Request("POST", url, *opts)
366}
367
Jon Perrittaaafa612016-02-21 18:23:38 -0600368// Put calls `Request` with the "PUT" HTTP verb.
369func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100370 if opts == nil {
371 opts = &RequestOpts{}
372 }
373
Brendan ODonnella69b3472015-04-27 13:59:41 -0500374 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100375 opts.RawBody = v
376 } else if JSONBody != nil {
377 opts.JSONBody = JSONBody
378 }
379
380 if JSONResponse != nil {
381 opts.JSONResponse = JSONResponse
382 }
383
384 return client.Request("PUT", url, *opts)
385}
386
Jon Perrittaaafa612016-02-21 18:23:38 -0600387// Patch calls `Request` with the "PATCH" HTTP verb.
388func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100389 if opts == nil {
390 opts = &RequestOpts{}
391 }
392
393 if v, ok := (JSONBody).(io.ReadSeeker); ok {
394 opts.RawBody = v
395 } else if JSONBody != nil {
396 opts.JSONBody = JSONBody
397 }
398
399 if JSONResponse != nil {
400 opts.JSONResponse = JSONResponse
401 }
402
403 return client.Request("PATCH", url, *opts)
404}
405
Jon Perrittaaafa612016-02-21 18:23:38 -0600406// Delete calls `Request` with the "DELETE" HTTP verb.
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100407func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
408 if opts == nil {
409 opts = &RequestOpts{}
410 }
411
412 return client.Request("DELETE", url, *opts)
413}