blob: 4f25589b54ca06fa60bd273d62f65372370c00b1 [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"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "net/http"
Jon Perritt2b5e3e12015-02-13 12:15:08 -070010 "strings"
Ash Wilson89eec332015-02-12 13:40:32 -050011)
12
Jon Perritt2b5e3e12015-02-13 12:15:08 -070013// DefaultUserAgent is the default User-Agent string set in the request header.
jrperrittb1013232016-02-10 19:01:53 -060014const DefaultUserAgent = "gophercloud/2.0.0"
Jon Perritt2b5e3e12015-02-13 12:15:08 -070015
16// UserAgent represents a User-Agent header.
17type UserAgent struct {
18 // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
19 // All the strings to prepend are accumulated and prepended in the Join method.
20 prepend []string
21}
22
23// Prepend prepends a user-defined string to the default User-Agent string. Users
24// may pass in one or more strings to prepend.
25func (ua *UserAgent) Prepend(s ...string) {
26 ua.prepend = append(s, ua.prepend...)
27}
28
29// Join concatenates all the user-defined User-Agend strings with the default
30// Gophercloud User-Agent string.
31func (ua *UserAgent) Join() string {
32 uaSlice := append(ua.prepend, DefaultUserAgent)
33 return strings.Join(uaSlice, " ")
34}
35
Jamie Hannafordb280dea2014-10-24 15:14:06 +020036// ProviderClient stores details that are required to interact with any
37// services within a specific provider's API.
Ash Wilson89466cc2014-08-29 11:27:39 -040038//
Jamie Hannafordb280dea2014-10-24 15:14:06 +020039// Generally, you acquire a ProviderClient by calling the NewClient method in
40// the appropriate provider's child package, providing whatever authentication
41// credentials are required.
Ash Wilson89466cc2014-08-29 11:27:39 -040042type ProviderClient struct {
Jamie Hannafordb280dea2014-10-24 15:14:06 +020043 // IdentityBase is the base URL used for a particular provider's identity
44 // service - it will be used when issuing authenticatation requests. It
45 // should point to the root resource of the identity service, not a specific
46 // identity version.
Ash Wilson09694b92014-09-09 14:08:27 -040047 IdentityBase string
48
Jamie Hannafordb280dea2014-10-24 15:14:06 +020049 // IdentityEndpoint is the identity endpoint. This may be a specific version
50 // of the identity service. If this is the case, this endpoint is used rather
51 // than querying versions first.
Ash Wilsonc6372fe2014-09-03 11:24:52 -040052 IdentityEndpoint string
53
Jamie Hannafordb280dea2014-10-24 15:14:06 +020054 // TokenID is the ID of the most recently issued valid token.
Ash Wilson89466cc2014-08-29 11:27:39 -040055 TokenID string
Ash Wilsonb8401a72014-09-08 17:07:49 -040056
Jamie Hannafordb280dea2014-10-24 15:14:06 +020057 // EndpointLocator describes how this provider discovers the endpoints for
58 // its constituent services.
Ash Wilsonb8401a72014-09-08 17:07:49 -040059 EndpointLocator EndpointLocator
Ash Wilson89eec332015-02-12 13:40:32 -050060
61 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
62 HTTPClient http.Client
Jon Perritt2b5e3e12015-02-13 12:15:08 -070063
64 // UserAgent represents the User-Agent header in the HTTP request.
65 UserAgent UserAgent
Jon Perrittf4052c62015-02-14 09:48:18 -070066
Jon Perrittf4052c62015-02-14 09:48:18 -070067 // ReauthFunc is the function used to re-authenticate the user if the request
68 // fails with a 401 HTTP response code. This a needed because there may be multiple
69 // authentication functions for different Identity service versions.
Jon Perritt6fe7c402015-02-17 12:24:53 -070070 ReauthFunc func() error
Ash Wilson89466cc2014-08-29 11:27:39 -040071}
72
Jamie Hannafordb280dea2014-10-24 15:14:06 +020073// AuthenticatedHeaders returns a map of HTTP headers that are common for all
74// authenticated service requests.
Ash Wilson89466cc2014-08-29 11:27:39 -040075func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
Ash Wilson89eec332015-02-12 13:40:32 -050076 if client.TokenID == "" {
77 return map[string]string{}
78 }
Ash Wilson89466cc2014-08-29 11:27:39 -040079 return map[string]string{"X-Auth-Token": client.TokenID}
80}
Ash Wilson89eec332015-02-12 13:40:32 -050081
82// RequestOpts customizes the behavior of the provider.Request() method.
83type RequestOpts struct {
84 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
85 // content type of the request will default to "application/json" unless overridden by MoreHeaders.
86 // It's an error to specify both a JSONBody and a RawBody.
87 JSONBody interface{}
jrperrittb1013232016-02-10 19:01:53 -060088 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
Ash Wilson89eec332015-02-12 13:40:32 -050089 // will be set unless one is provided explicitly by MoreHeaders.
jrperrittb1013232016-02-10 19:01:53 -060090 RawBody io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -050091 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
92 // JSON.
Ash Wilson2491b4c2015-02-12 16:13:39 -050093 JSONResponse interface{}
Ash Wilson89eec332015-02-12 13:40:32 -050094 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
95 // the response has a different code, an error will be returned.
96 OkCodes []int
Ash Wilson89eec332015-02-12 13:40:32 -050097 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
98 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
99 // the default Accept header or an inferred Content-Type, for example.
100 MoreHeaders map[string]string
101}
102
Ash Wilson89eec332015-02-12 13:40:32 -0500103var applicationJSON = "application/json"
104
105// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
106// header will automatically be provided.
107func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
jrperrittb1013232016-02-10 19:01:53 -0600108 var body io.Reader
Ash Wilson89eec332015-02-12 13:40:32 -0500109 var contentType *string
110
111 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
Brendan ODonnella69b3472015-04-27 13:59:41 -0500112 // io.ReadSeeker as-is. Default the content-type to application/json.
Ash Wilson89eec332015-02-12 13:40:32 -0500113 if options.JSONBody != nil {
114 if options.RawBody != nil {
115 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
116 }
117
118 rendered, err := json.Marshal(options.JSONBody)
119 if err != nil {
120 return nil, err
121 }
122
123 body = bytes.NewReader(rendered)
124 contentType = &applicationJSON
125 }
126
127 if options.RawBody != nil {
128 body = options.RawBody
129 }
130
131 // Construct the http.Request.
Ash Wilson89eec332015-02-12 13:40:32 -0500132 req, err := http.NewRequest(method, url, body)
133 if err != nil {
134 return nil, err
135 }
136
137 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
138 // modify or omit any header.
Ash Wilson89eec332015-02-12 13:40:32 -0500139 if contentType != nil {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500140 req.Header.Set("Content-Type", *contentType)
Ash Wilson89eec332015-02-12 13:40:32 -0500141 }
Ash Wilson54d62fa2015-02-12 15:09:46 -0500142 req.Header.Set("Accept", applicationJSON)
Ash Wilson89eec332015-02-12 13:40:32 -0500143
144 for k, v := range client.AuthenticatedHeaders() {
145 req.Header.Add(k, v)
146 }
147
Jon Perrittf0a1fee2015-02-13 12:53:23 -0700148 // Set the User-Agent header
149 req.Header.Set("User-Agent", client.UserAgent.Join())
150
Ash Wilson89eec332015-02-12 13:40:32 -0500151 if options.MoreHeaders != nil {
152 for k, v := range options.MoreHeaders {
153 if v != "" {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500154 req.Header.Set(k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500155 } else {
156 req.Header.Del(k)
157 }
158 }
159 }
160
Kostiantyn Yarovyi3fa30bb2015-11-25 17:21:03 +0200161 // Set connection parameter to close the connection immediately when we've got the response
162 req.Close = true
Jon Perrittaaafa612016-02-21 18:23:38 -0600163
Jon Perritt2b5e3e12015-02-13 12:15:08 -0700164 // Issue the request.
Ash Wilson89eec332015-02-12 13:40:32 -0500165 resp, err := client.HTTPClient.Do(req)
166 if err != nil {
167 return nil, err
168 }
169
Jon Perritt6fe7c402015-02-17 12:24:53 -0700170 if resp.StatusCode == http.StatusUnauthorized {
171 if client.ReauthFunc != nil {
172 err = client.ReauthFunc()
Jon Perrittf4052c62015-02-14 09:48:18 -0700173 if err != nil {
174 return nil, fmt.Errorf("Error trying to re-authenticate: %s", err)
175 }
jrperrittb1013232016-02-10 19:01:53 -0600176 if seeker, ok := options.RawBody.(io.ReadSeeker); ok && options.RawBody != nil {
177 seeker.Seek(0, 0)
Jon Perrittfcedd7b2015-06-15 19:41:01 -0600178 }
Fredi Pevcina979be92015-10-20 09:13:29 +0200179 resp.Body.Close()
Jon Perrittf4052c62015-02-14 09:48:18 -0700180 resp, err = client.Request(method, url, options)
181 if err != nil {
182 return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err)
183 }
Clinton Kitsonfcd283a2016-01-07 09:00:56 -0800184
185 return resp, nil
Jon Perrittf4052c62015-02-14 09:48:18 -0700186 }
187 }
188
Jamie Hannaford647cea52015-03-23 17:15:07 +0100189 // Allow default OkCodes if none explicitly set
190 if options.OkCodes == nil {
191 options.OkCodes = defaultOkCodes(method)
192 }
193
194 // Validate the HTTP response status.
195 var ok bool
196 for _, code := range options.OkCodes {
197 if resp.StatusCode == code {
198 ok = true
199 break
Ash Wilson89eec332015-02-12 13:40:32 -0500200 }
Jamie Hannaford647cea52015-03-23 17:15:07 +0100201 }
202 if !ok {
203 body, _ := ioutil.ReadAll(resp.Body)
204 resp.Body.Close()
jrperrittb1013232016-02-10 19:01:53 -0600205 return resp, &ErrUnexpectedResponseCode{
Jamie Hannaford647cea52015-03-23 17:15:07 +0100206 URL: url,
207 Method: method,
208 Expected: options.OkCodes,
209 Actual: resp.StatusCode,
210 Body: body,
Ash Wilson89eec332015-02-12 13:40:32 -0500211 }
212 }
213
214 // Parse the response body as JSON, if requested to do so.
Ash Wilson89eec332015-02-12 13:40:32 -0500215 if options.JSONResponse != nil {
216 defer resp.Body.Close()
Pratik Mallyaee675fd2015-09-14 14:07:30 -0500217 if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
218 return nil, err
219 }
Ash Wilson89eec332015-02-12 13:40:32 -0500220 }
221
222 return resp, nil
223}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100224
225func defaultOkCodes(method string) []int {
226 switch {
227 case method == "GET":
228 return []int{200}
229 case method == "POST":
230 return []int{201, 202}
231 case method == "PUT":
232 return []int{201, 202}
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100233 case method == "PATCH":
234 return []int{200, 204}
Jamie Hannaford647cea52015-03-23 17:15:07 +0100235 case method == "DELETE":
236 return []int{202, 204}
237 }
238
239 return []int{}
240}
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100241
Jon Perrittaaafa612016-02-21 18:23:38 -0600242// Get calls `Request` with the "GET" HTTP verb.
243func (client *ProviderClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100244 if opts == nil {
245 opts = &RequestOpts{}
246 }
247 if JSONResponse != nil {
248 opts.JSONResponse = JSONResponse
249 }
250 return client.Request("GET", url, *opts)
251}
252
Jon Perrittaaafa612016-02-21 18:23:38 -0600253// Post calls `Request` with the "POST" HTTP verb.
254func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100255 if opts == nil {
256 opts = &RequestOpts{}
257 }
258
Brendan ODonnella69b3472015-04-27 13:59:41 -0500259 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100260 opts.RawBody = v
261 } else if JSONBody != nil {
262 opts.JSONBody = JSONBody
263 }
264
265 if JSONResponse != nil {
266 opts.JSONResponse = JSONResponse
267 }
268
269 return client.Request("POST", url, *opts)
270}
271
Jon Perrittaaafa612016-02-21 18:23:38 -0600272// Put calls `Request` with the "PUT" HTTP verb.
273func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100274 if opts == nil {
275 opts = &RequestOpts{}
276 }
277
Brendan ODonnella69b3472015-04-27 13:59:41 -0500278 if v, ok := (JSONBody).(io.ReadSeeker); ok {
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100279 opts.RawBody = v
280 } else if JSONBody != nil {
281 opts.JSONBody = JSONBody
282 }
283
284 if JSONResponse != nil {
285 opts.JSONResponse = JSONResponse
286 }
287
288 return client.Request("PUT", url, *opts)
289}
290
Jon Perrittaaafa612016-02-21 18:23:38 -0600291// Patch calls `Request` with the "PATCH" HTTP verb.
292func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) {
Krzysztof Kwapisiewicz136d2c22016-02-03 15:36:06 +0100293 if opts == nil {
294 opts = &RequestOpts{}
295 }
296
297 if v, ok := (JSONBody).(io.ReadSeeker); ok {
298 opts.RawBody = v
299 } else if JSONBody != nil {
300 opts.JSONBody = JSONBody
301 }
302
303 if JSONResponse != nil {
304 opts.JSONResponse = JSONResponse
305 }
306
307 return client.Request("PATCH", url, *opts)
308}
309
Jon Perrittaaafa612016-02-21 18:23:38 -0600310// Delete calls `Request` with the "DELETE" HTTP verb.
Jamie Hannaford2a9e74f2015-03-24 14:55:24 +0100311func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) {
312 if opts == nil {
313 opts = &RequestOpts{}
314 }
315
316 return client.Request("DELETE", url, *opts)
317}