blob: f868098e3bb5a4f1b5f58895298e33ee2aabe48e [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"
Ash Wilson89eec332015-02-12 13:40:32 -05006 "io"
7 "io/ioutil"
8 "net/http"
Jon Perritt2b5e3e12015-02-13 12:15:08 -07009 "strings"
Ash Wilson89eec332015-02-12 13:40:32 -050010)
11
Jon Perritt2b5e3e12015-02-13 12:15:08 -070012// DefaultUserAgent is the default User-Agent string set in the request header.
jrperrittb1013232016-02-10 19:01:53 -060013const DefaultUserAgent = "gophercloud/2.0.0"
Jon Perritt2b5e3e12015-02-13 12:15:08 -070014
15// UserAgent represents a User-Agent header.
16type UserAgent struct {
17 // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
18 // All the strings to prepend are accumulated and prepended in the Join method.
19 prepend []string
20}
21
22// Prepend prepends a user-defined string to the default User-Agent string. Users
23// may pass in one or more strings to prepend.
24func (ua *UserAgent) Prepend(s ...string) {
25 ua.prepend = append(s, ua.prepend...)
26}
27
28// Join concatenates all the user-defined User-Agend strings with the default
29// Gophercloud User-Agent string.
30func (ua *UserAgent) Join() string {
31 uaSlice := append(ua.prepend, DefaultUserAgent)
32 return strings.Join(uaSlice, " ")
33}
34
Jamie Hannafordb280dea2014-10-24 15:14:06 +020035// ProviderClient stores details that are required to interact with any
36// services within a specific provider's API.
Ash Wilson89466cc2014-08-29 11:27:39 -040037//
Jamie Hannafordb280dea2014-10-24 15:14:06 +020038// Generally, you acquire a ProviderClient by calling the NewClient method in
39// the appropriate provider's child package, providing whatever authentication
40// credentials are required.
Ash Wilson89466cc2014-08-29 11:27:39 -040041type ProviderClient struct {
Jamie Hannafordb280dea2014-10-24 15:14:06 +020042 // IdentityBase is the base URL used for a particular provider's identity
43 // service - it will be used when issuing authenticatation requests. It
44 // should point to the root resource of the identity service, not a specific
45 // identity version.
Ash Wilson09694b92014-09-09 14:08:27 -040046 IdentityBase string
47
Jamie Hannafordb280dea2014-10-24 15:14:06 +020048 // IdentityEndpoint is the identity endpoint. This may be a specific version
49 // of the identity service. If this is the case, this endpoint is used rather
50 // than querying versions first.
Ash Wilsonc6372fe2014-09-03 11:24:52 -040051 IdentityEndpoint string
52
Jamie Hannafordb280dea2014-10-24 15:14:06 +020053 // TokenID is the ID of the most recently issued valid token.
Ash Wilson89466cc2014-08-29 11:27:39 -040054 TokenID string
Ash Wilsonb8401a72014-09-08 17:07:49 -040055
Jamie Hannafordb280dea2014-10-24 15:14:06 +020056 // EndpointLocator describes how this provider discovers the endpoints for
57 // its constituent services.
Ash Wilsonb8401a72014-09-08 17:07:49 -040058 EndpointLocator EndpointLocator
Ash Wilson89eec332015-02-12 13:40:32 -050059
60 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
61 HTTPClient http.Client
Jon Perritt2b5e3e12015-02-13 12:15:08 -070062
63 // UserAgent represents the User-Agent header in the HTTP request.
64 UserAgent UserAgent
Jon Perrittf4052c62015-02-14 09:48:18 -070065
Jon Perrittf4052c62015-02-14 09:48:18 -070066 // ReauthFunc is the function used to re-authenticate the user if the request
67 // fails with a 401 HTTP response code. This a needed because there may be multiple
68 // authentication functions for different Identity service versions.
Jon Perritt6fe7c402015-02-17 12:24:53 -070069 ReauthFunc func() error
Jon Perrittf094fef2016-03-07 01:41:59 -060070
71 Debug bool
Ash Wilson89466cc2014-08-29 11:27:39 -040072}
73
Jamie Hannafordb280dea2014-10-24 15:14:06 +020074// AuthenticatedHeaders returns a map of HTTP headers that are common for all
75// authenticated service requests.
Ash Wilson89466cc2014-08-29 11:27:39 -040076func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
Ash Wilson89eec332015-02-12 13:40:32 -050077 if client.TokenID == "" {
78 return map[string]string{}
79 }
Ash Wilson89466cc2014-08-29 11:27:39 -040080 return map[string]string{"X-Auth-Token": client.TokenID}
81}
Ash Wilson89eec332015-02-12 13:40:32 -050082
83// RequestOpts customizes the behavior of the provider.Request() method.
84type RequestOpts struct {
85 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
86 // content type of the request will default to "application/json" unless overridden by MoreHeaders.
87 // It's an error to specify both a JSONBody and a RawBody.
88 JSONBody interface{}
jrperrittb1013232016-02-10 19:01:53 -060089 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
Ash Wilson89eec332015-02-12 13:40:32 -050090 // will be set unless one is provided explicitly by MoreHeaders.
jrperrittb1013232016-02-10 19:01:53 -060091 RawBody io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -050092 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
93 // JSON.
Ash Wilson2491b4c2015-02-12 16:13:39 -050094 JSONResponse interface{}
Ash Wilson89eec332015-02-12 13:40:32 -050095 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
96 // the response has a different code, an error will be returned.
97 OkCodes []int
Ash Wilson89eec332015-02-12 13:40:32 -050098 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
99 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
100 // the default Accept header or an inferred Content-Type, for example.
101 MoreHeaders map[string]string
Jon Perritt4024a022016-02-29 19:58:56 -0600102 // ErrorContext specifies the resource error type to return if an error is encountered.
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600103 // This lets resources override default error messages based on the response status code.
104 ErrorContext error
Ash Wilson89eec332015-02-12 13:40:32 -0500105}
106
Jon Perritt4024a022016-02-29 19:58:56 -0600107func (r *RequestOpts) ToRequestOpts() (*RequestOpts, error) {
108 return r, nil
109}
110
111type RequestOptsBuilder interface {
112 ToRequestOpts() (*RequestOpts, error)
113}
114
Ash Wilson89eec332015-02-12 13:40:32 -0500115var applicationJSON = "application/json"
116
117// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
118// header will automatically be provided.
Jon Perritt4024a022016-02-29 19:58:56 -0600119func (client *ProviderClient) Request(method, url string, optsBuilder RequestOptsBuilder) (*http.Response, error) {
jrperrittb1013232016-02-10 19:01:53 -0600120 var body io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -0500121 var contentType *string
122
Jon Perritt4024a022016-02-29 19:58:56 -0600123 options, err := optsBuilder.ToRequestOpts()
124 if err != nil {
125 return nil, err
126 }
127
Ash Wilson89eec332015-02-12 13:40:32 -0500128 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
Brendan ODonnella69b3472015-04-27 13:59:41 -0500129 // io.ReadSeeker as-is. Default the content-type to application/json.
Ash Wilson89eec332015-02-12 13:40:32 -0500130 if options.JSONBody != nil {
131 if options.RawBody != nil {
132 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
133 }
134
135 rendered, err := json.Marshal(options.JSONBody)
136 if err != nil {
137 return nil, err
138 }
139
140 body = bytes.NewReader(rendered)
141 contentType = &applicationJSON
142 }
143
144 if options.RawBody != nil {
145 body = options.RawBody
146 }
147
148 // Construct the http.Request.
Ash Wilson89eec332015-02-12 13:40:32 -0500149 req, err := http.NewRequest(method, url, body)
150 if err != nil {
151 return nil, err
152 }
153
154 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
155 // modify or omit any header.
Ash Wilson89eec332015-02-12 13:40:32 -0500156 if contentType != nil {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500157 req.Header.Set("Content-Type", *contentType)
Ash Wilson89eec332015-02-12 13:40:32 -0500158 }
Ash Wilson54d62fa2015-02-12 15:09:46 -0500159 req.Header.Set("Accept", applicationJSON)
Ash Wilson89eec332015-02-12 13:40:32 -0500160
161 for k, v := range client.AuthenticatedHeaders() {
162 req.Header.Add(k, v)
163 }
164
Jon Perrittf0a1fee2015-02-13 12:53:23 -0700165 // Set the User-Agent header
166 req.Header.Set("User-Agent", client.UserAgent.Join())
167
Ash Wilson89eec332015-02-12 13:40:32 -0500168 if options.MoreHeaders != nil {
169 for k, v := range options.MoreHeaders {
170 if v != "" {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500171 req.Header.Set(k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500172 } else {
173 req.Header.Del(k)
174 }
175 }
176 }
177
Kostiantyn Yarovyi3fa30bb2015-11-25 17:21:03 +0200178 // Set connection parameter to close the connection immediately when we've got the response
179 req.Close = true
Jon Perrittaaafa612016-02-21 18:23:38 -0600180
Jon Perritt2b5e3e12015-02-13 12:15:08 -0700181 // Issue the request.
Ash Wilson89eec332015-02-12 13:40:32 -0500182 resp, err := client.HTTPClient.Do(req)
183 if err != nil {
184 return nil, err
185 }
186
Jamie Hannaford647cea52015-03-23 17:15:07 +0100187 // Allow default OkCodes if none explicitly set
188 if options.OkCodes == nil {
189 options.OkCodes = defaultOkCodes(method)
190 }
191
192 // Validate the HTTP response status.
193 var ok bool
194 for _, code := range options.OkCodes {
195 if resp.StatusCode == code {
196 ok = true
197 break
Ash Wilson89eec332015-02-12 13:40:32 -0500198 }
Jamie Hannaford647cea52015-03-23 17:15:07 +0100199 }
Jon Perritt28256b32016-02-29 03:06:36 -0600200
Jamie Hannaford647cea52015-03-23 17:15:07 +0100201 if !ok {
202 body, _ := ioutil.ReadAll(resp.Body)
203 resp.Body.Close()
Jon Perrittf094fef2016-03-07 01:41:59 -0600204 //pc := make([]uintptr, 1)
Jon Perritt28256b32016-02-29 03:06:36 -0600205 //runtime.Callers(2, pc)
206 //f := runtime.FuncForPC(pc[0])
Jon Perrittf094fef2016-03-07 01:41:59 -0600207 respErr := ErrUnexpectedResponseCode{
Jamie Hannaford647cea52015-03-23 17:15:07 +0100208 URL: url,
209 Method: method,
210 Expected: options.OkCodes,
211 Actual: resp.StatusCode,
212 Body: body,
Ash Wilson89eec332015-02-12 13:40:32 -0500213 }
Jon Perritt28256b32016-02-29 03:06:36 -0600214 respErr.Function = "gophercloud.ProviderClient.Request"
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600215
216 errType := options.ErrorContext
217 switch resp.StatusCode {
218 case http.StatusBadRequest:
219 err = ErrDefault400{respErr}
220 if error400er, ok := errType.(Err400er); ok {
221 err = error400er.Error400(respErr)
222 }
223 case http.StatusUnauthorized:
224 if client.ReauthFunc != nil {
225 err = client.ReauthFunc()
226 if err != nil {
Jon Perritt28256b32016-02-29 03:06:36 -0600227 e := &ErrUnableToReauthenticate{}
Jon Perrittf094fef2016-03-07 01:41:59 -0600228 e.ErrOriginal = respErr
Jon Perritt28256b32016-02-29 03:06:36 -0600229 return nil, e
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600230 }
231 if options.RawBody != nil {
Jon Perritt28256b32016-02-29 03:06:36 -0600232 if seeker, ok := options.RawBody.(io.Seeker); ok {
233 seeker.Seek(0, 0)
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600234 }
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600235 }
236 resp, err = client.Request(method, url, options)
237 if err != nil {
238 switch err.(type) {
239 case *ErrUnexpectedResponseCode:
Jon Perritt28256b32016-02-29 03:06:36 -0600240 e := &ErrErrorAfterReauthentication{}
Jon Perrittf094fef2016-03-07 01:41:59 -0600241 e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
Jon Perritt28256b32016-02-29 03:06:36 -0600242 return nil, e
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600243 default:
Jon Perritt28256b32016-02-29 03:06:36 -0600244 e := &ErrErrorAfterReauthentication{}
Jon Perrittf094fef2016-03-07 01:41:59 -0600245 e.ErrOriginal = err
Jon Perritt28256b32016-02-29 03:06:36 -0600246 return nil, e
Jon Perritte0f9e4f2016-02-21 21:41:03 -0600247 }
248 }
249 return resp, nil
250 }
251 err = ErrDefault401{respErr}
252 if error401er, ok := errType.(Err401er); ok {
253 err = error401er.Error401(respErr)
254 }
255 case http.StatusNotFound:
256 err = ErrDefault404{respErr}
257 if error404er, ok := errType.(Err404er); ok {
258 err = error404er.Error404(respErr)
259 }
260 case http.StatusMethodNotAllowed:
261 err = ErrDefault405{respErr}
262 if error405er, ok := errType.(Err405er); ok {
263 err = error405er.Error405(respErr)
264 }
265 case http.StatusRequestTimeout:
266 err = ErrDefault408{respErr}
267 if error408er, ok := errType.(Err408er); ok {
268 err = error408er.Error408(respErr)
269 }
270 case 429:
271 err = ErrDefault429{respErr}
272 if error429er, ok := errType.(Err429er); ok {
273 err = error429er.Error429(respErr)
274 }
275 case http.StatusInternalServerError:
276 err = ErrDefault500{respErr}
277 if error500er, ok := errType.(Err500er); ok {
278 err = error500er.Error500(respErr)
279 }
280 case http.StatusServiceUnavailable:
281 err = ErrDefault503{respErr}
282 if error503er, ok := errType.(Err503er); ok {
283 err = error503er.Error503(respErr)
284 }
285 }
286
287 if err == nil {
288 err = respErr
289 }
290
291 return resp, err
Ash Wilson89eec332015-02-12 13:40:32 -0500292 }
293
294 // Parse the response body as JSON, if requested to do so.
Ash Wilson89eec332015-02-12 13:40:32 -0500295 if options.JSONResponse != nil {
296 defer resp.Body.Close()
Pratik Mallyaee675fd2015-09-14 14:07:30 -0500297 if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
298 return nil, err
299 }
Ash Wilson89eec332015-02-12 13:40:32 -0500300 }
301
302 return resp, nil
303}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100304
305func defaultOkCodes(method string) []int {
306 switch {
307 case method == "GET":
308 return []int{200}
309 case method == "POST":
310 return []int{201, 202}
311 case method == "PUT":
312 return []int{201, 202}
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100313 case method == "PATCH":
314 return []int{200, 204}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100315 case method == "DELETE":
316 return []int{202, 204}
317 }
318
319 return []int{}
320}
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100321
Jon Perrittaaafa612016-02-21 18:23:38 -0600322// Get calls `Request` with the "GET" HTTP verb.
323func (client *ProviderClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100324 if opts == nil {
325 opts = &RequestOpts{}
326 }
327 if JSONResponse != nil {
328 opts.JSONResponse = JSONResponse
329 }
Jon Perritt4024a022016-02-29 19:58:56 -0600330 return client.Request("GET", url, opts)
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100331}
332
Jon Perrittaaafa612016-02-21 18:23:38 -0600333// Post calls `Request` with the "POST" HTTP verb.
334func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100335 if opts == nil {
336 opts = &RequestOpts{}
337 }
338
Brendan ODonnella69b3472015-04-27 13:59:41 -0500339 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100340 opts.RawBody = v
341 } else if JSONBody != nil {
342 opts.JSONBody = JSONBody
343 }
344
345 if JSONResponse != nil {
346 opts.JSONResponse = JSONResponse
347 }
348
Jon Perritt4024a022016-02-29 19:58:56 -0600349 return client.Request("POST", url, opts)
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100350}
351
Jon Perrittaaafa612016-02-21 18:23:38 -0600352// Put calls `Request` with the "PUT" HTTP verb.
353func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100354 if opts == nil {
355 opts = &RequestOpts{}
356 }
357
Brendan ODonnella69b3472015-04-27 13:59:41 -0500358 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100359 opts.RawBody = v
360 } else if JSONBody != nil {
361 opts.JSONBody = JSONBody
362 }
363
364 if JSONResponse != nil {
365 opts.JSONResponse = JSONResponse
366 }
367
Jon Perritt4024a022016-02-29 19:58:56 -0600368 return client.Request("PUT", url, opts)
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100369}
370
Jon Perrittaaafa612016-02-21 18:23:38 -0600371// Patch calls `Request` with the "PATCH" HTTP verb.
372func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100373 if opts == nil {
374 opts = &RequestOpts{}
375 }
376
377 if v, ok := (JSONBody).(io.ReadSeeker); ok {
378 opts.RawBody = v
379 } else if JSONBody != nil {
380 opts.JSONBody = JSONBody
381 }
382
383 if JSONResponse != nil {
384 opts.JSONResponse = JSONResponse
385 }
386
Jon Perritt4024a022016-02-29 19:58:56 -0600387 return client.Request("PATCH", url, opts)
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100388}
389
Jon Perrittaaafa612016-02-21 18:23:38 -0600390// Delete calls `Request` with the "DELETE" HTTP verb.
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100391func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
392 if opts == nil {
393 opts = &RequestOpts{}
394 }
395
Jon Perritt4024a022016-02-29 19:58:56 -0600396 return client.Request("DELETE", url, opts)
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100397}