| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 1 | package gophercloud | 
|  | 2 |  | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 3 | import ( | 
|  | 4 | "bytes" | 
|  | 5 | "encoding/json" | 
|  | 6 | "fmt" | 
|  | 7 | "io" | 
|  | 8 | "io/ioutil" | 
|  | 9 | "net/http" | 
| Jon Perritt | 2b5e3e1 | 2015-02-13 12:15:08 -0700 | [diff] [blame] | 10 | "strings" | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 11 | ) | 
|  | 12 |  | 
| Jon Perritt | 2b5e3e1 | 2015-02-13 12:15:08 -0700 | [diff] [blame] | 13 | // DefaultUserAgent is the default User-Agent string set in the request header. | 
| Jamie Hannaford | b134f4c | 2015-04-07 12:05:18 +0200 | [diff] [blame] | 14 | const DefaultUserAgent = "gophercloud/1.0.0" | 
| Jon Perritt | 2b5e3e1 | 2015-02-13 12:15:08 -0700 | [diff] [blame] | 15 |  | 
|  | 16 | // UserAgent represents a User-Agent header. | 
|  | 17 | type 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. | 
|  | 25 | func (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. | 
|  | 31 | func (ua *UserAgent) Join() string { | 
|  | 32 | uaSlice := append(ua.prepend, DefaultUserAgent) | 
|  | 33 | return strings.Join(uaSlice, " ") | 
|  | 34 | } | 
|  | 35 |  | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 36 | // ProviderClient stores details that are required to interact with any | 
|  | 37 | // services within a specific provider's API. | 
| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 38 | // | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 39 | // 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 Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 42 | type ProviderClient struct { | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 43 | // 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 Wilson | 09694b9 | 2014-09-09 14:08:27 -0400 | [diff] [blame] | 47 | IdentityBase string | 
|  | 48 |  | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 49 | // 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 Wilson | c6372fe | 2014-09-03 11:24:52 -0400 | [diff] [blame] | 52 | IdentityEndpoint string | 
|  | 53 |  | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 54 | // TokenID is the ID of the most recently issued valid token. | 
| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 55 | TokenID string | 
| Ash Wilson | b8401a7 | 2014-09-08 17:07:49 -0400 | [diff] [blame] | 56 |  | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 57 | // EndpointLocator describes how this provider discovers the endpoints for | 
|  | 58 | // its constituent services. | 
| Ash Wilson | b8401a7 | 2014-09-08 17:07:49 -0400 | [diff] [blame] | 59 | EndpointLocator EndpointLocator | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 60 |  | 
|  | 61 | // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. | 
|  | 62 | HTTPClient http.Client | 
| Jon Perritt | 2b5e3e1 | 2015-02-13 12:15:08 -0700 | [diff] [blame] | 63 |  | 
|  | 64 | // UserAgent represents the User-Agent header in the HTTP request. | 
|  | 65 | UserAgent UserAgent | 
| Jon Perritt | f4052c6 | 2015-02-14 09:48:18 -0700 | [diff] [blame] | 66 |  | 
| Jon Perritt | f4052c6 | 2015-02-14 09:48:18 -0700 | [diff] [blame] | 67 | // 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 Perritt | 6fe7c40 | 2015-02-17 12:24:53 -0700 | [diff] [blame] | 70 | ReauthFunc func() error | 
| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 71 | } | 
|  | 72 |  | 
| Jamie Hannaford | b280dea | 2014-10-24 15:14:06 +0200 | [diff] [blame] | 73 | // AuthenticatedHeaders returns a map of HTTP headers that are common for all | 
|  | 74 | // authenticated service requests. | 
| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 75 | func (client *ProviderClient) AuthenticatedHeaders() map[string]string { | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 76 | if client.TokenID == "" { | 
|  | 77 | return map[string]string{} | 
|  | 78 | } | 
| Ash Wilson | 89466cc | 2014-08-29 11:27:39 -0400 | [diff] [blame] | 79 | return map[string]string{"X-Auth-Token": client.TokenID} | 
|  | 80 | } | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 81 |  | 
|  | 82 | // RequestOpts customizes the behavior of the provider.Request() method. | 
|  | 83 | type 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{} | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 88 | // RawBody contains an io.ReadSeeker that will be consumed by the request directly. No content-type | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 89 | // will be set unless one is provided explicitly by MoreHeaders. | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 90 | RawBody io.ReadSeeker | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 91 |  | 
|  | 92 | // JSONResponse, if provided, will be populated with the contents of the response body parsed as | 
|  | 93 | // JSON. | 
| Ash Wilson | 2491b4c | 2015-02-12 16:13:39 -0500 | [diff] [blame] | 94 | JSONResponse interface{} | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 95 | // 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 | 
|  | 98 |  | 
|  | 99 | // 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 | 
|  | 103 | } | 
|  | 104 |  | 
|  | 105 | // UnexpectedResponseCodeError is returned by the Request method when a response code other than | 
|  | 106 | // those listed in OkCodes is encountered. | 
|  | 107 | type UnexpectedResponseCodeError struct { | 
|  | 108 | URL      string | 
|  | 109 | Method   string | 
|  | 110 | Expected []int | 
|  | 111 | Actual   int | 
|  | 112 | Body     []byte | 
|  | 113 | } | 
|  | 114 |  | 
|  | 115 | func (err *UnexpectedResponseCodeError) Error() string { | 
|  | 116 | return fmt.Sprintf( | 
|  | 117 | "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", | 
|  | 118 | err.Expected, err.Method, err.URL, err.Actual, err.Body, | 
|  | 119 | ) | 
|  | 120 | } | 
|  | 121 |  | 
|  | 122 | var applicationJSON = "application/json" | 
|  | 123 |  | 
|  | 124 | // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication | 
|  | 125 | // header will automatically be provided. | 
|  | 126 | func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) { | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 127 | var body io.ReadSeeker | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 128 | var contentType *string | 
|  | 129 |  | 
|  | 130 | // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 131 | // io.ReadSeeker as-is. Default the content-type to application/json. | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 132 | if options.JSONBody != nil { | 
|  | 133 | if options.RawBody != nil { | 
|  | 134 | panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") | 
|  | 135 | } | 
|  | 136 |  | 
|  | 137 | rendered, err := json.Marshal(options.JSONBody) | 
|  | 138 | if err != nil { | 
|  | 139 | return nil, err | 
|  | 140 | } | 
|  | 141 |  | 
|  | 142 | body = bytes.NewReader(rendered) | 
|  | 143 | contentType = &applicationJSON | 
|  | 144 | } | 
|  | 145 |  | 
|  | 146 | if options.RawBody != nil { | 
|  | 147 | body = options.RawBody | 
|  | 148 | } | 
|  | 149 |  | 
|  | 150 | // Construct the http.Request. | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 151 | req, err := http.NewRequest(method, url, body) | 
|  | 152 | if err != nil { | 
|  | 153 | return nil, err | 
|  | 154 | } | 
|  | 155 |  | 
|  | 156 | // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to | 
|  | 157 | // modify or omit any header. | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 158 | if contentType != nil { | 
| Ash Wilson | 54d62fa | 2015-02-12 15:09:46 -0500 | [diff] [blame] | 159 | req.Header.Set("Content-Type", *contentType) | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 160 | } | 
| Ash Wilson | 54d62fa | 2015-02-12 15:09:46 -0500 | [diff] [blame] | 161 | req.Header.Set("Accept", applicationJSON) | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 162 |  | 
|  | 163 | for k, v := range client.AuthenticatedHeaders() { | 
|  | 164 | req.Header.Add(k, v) | 
|  | 165 | } | 
|  | 166 |  | 
| Jon Perritt | f0a1fee | 2015-02-13 12:53:23 -0700 | [diff] [blame] | 167 | // Set the User-Agent header | 
|  | 168 | req.Header.Set("User-Agent", client.UserAgent.Join()) | 
|  | 169 |  | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 170 | if options.MoreHeaders != nil { | 
|  | 171 | for k, v := range options.MoreHeaders { | 
|  | 172 | if v != "" { | 
| Ash Wilson | 54d62fa | 2015-02-12 15:09:46 -0500 | [diff] [blame] | 173 | req.Header.Set(k, v) | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 174 | } else { | 
|  | 175 | req.Header.Del(k) | 
|  | 176 | } | 
|  | 177 | } | 
|  | 178 | } | 
|  | 179 |  | 
| Kostiantyn Yarovyi | 3fa30bb | 2015-11-25 17:21:03 +0200 | [diff] [blame] | 180 | // Set connection parameter to close the connection immediately when we've got the response | 
|  | 181 | req.Close = true | 
|  | 182 |  | 
| Jon Perritt | 2b5e3e1 | 2015-02-13 12:15:08 -0700 | [diff] [blame] | 183 | // Issue the request. | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 184 | resp, err := client.HTTPClient.Do(req) | 
|  | 185 | if err != nil { | 
|  | 186 | return nil, err | 
|  | 187 | } | 
|  | 188 |  | 
| Jon Perritt | 6fe7c40 | 2015-02-17 12:24:53 -0700 | [diff] [blame] | 189 | if resp.StatusCode == http.StatusUnauthorized { | 
|  | 190 | if client.ReauthFunc != nil { | 
|  | 191 | err = client.ReauthFunc() | 
| Jon Perritt | f4052c6 | 2015-02-14 09:48:18 -0700 | [diff] [blame] | 192 | if err != nil { | 
|  | 193 | return nil, fmt.Errorf("Error trying to re-authenticate: %s", err) | 
|  | 194 | } | 
| Jon Perritt | fcedd7b | 2015-06-15 19:41:01 -0600 | [diff] [blame] | 195 | if options.RawBody != nil { | 
|  | 196 | options.RawBody.Seek(0, 0) | 
|  | 197 | } | 
| Fredi Pevcin | a979be9 | 2015-10-20 09:13:29 +0200 | [diff] [blame] | 198 | resp.Body.Close() | 
| Jon Perritt | f4052c6 | 2015-02-14 09:48:18 -0700 | [diff] [blame] | 199 | resp, err = client.Request(method, url, options) | 
|  | 200 | if err != nil { | 
|  | 201 | return nil, fmt.Errorf("Successfully re-authenticated, but got error executing request: %s", err) | 
|  | 202 | } | 
| Clinton Kitson | fcd283a | 2016-01-07 09:00:56 -0800 | [diff] [blame] | 203 |  | 
|  | 204 | return resp, nil | 
| Jon Perritt | f4052c6 | 2015-02-14 09:48:18 -0700 | [diff] [blame] | 205 | } | 
|  | 206 | } | 
|  | 207 |  | 
| Jamie Hannaford | 647cea5 | 2015-03-23 17:15:07 +0100 | [diff] [blame] | 208 | // Allow default OkCodes if none explicitly set | 
|  | 209 | if options.OkCodes == nil { | 
|  | 210 | options.OkCodes = defaultOkCodes(method) | 
|  | 211 | } | 
|  | 212 |  | 
|  | 213 | // Validate the HTTP response status. | 
|  | 214 | var ok bool | 
|  | 215 | for _, code := range options.OkCodes { | 
|  | 216 | if resp.StatusCode == code { | 
|  | 217 | ok = true | 
|  | 218 | break | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 219 | } | 
| Jamie Hannaford | 647cea5 | 2015-03-23 17:15:07 +0100 | [diff] [blame] | 220 | } | 
|  | 221 | if !ok { | 
|  | 222 | body, _ := ioutil.ReadAll(resp.Body) | 
|  | 223 | resp.Body.Close() | 
|  | 224 | return resp, &UnexpectedResponseCodeError{ | 
|  | 225 | URL:      url, | 
|  | 226 | Method:   method, | 
|  | 227 | Expected: options.OkCodes, | 
|  | 228 | Actual:   resp.StatusCode, | 
|  | 229 | Body:     body, | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 230 | } | 
|  | 231 | } | 
|  | 232 |  | 
|  | 233 | // Parse the response body as JSON, if requested to do so. | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 234 | if options.JSONResponse != nil { | 
|  | 235 | defer resp.Body.Close() | 
| Pratik Mallya | ee675fd | 2015-09-14 14:07:30 -0500 | [diff] [blame] | 236 | if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { | 
|  | 237 | return nil, err | 
|  | 238 | } | 
| Ash Wilson | 89eec33 | 2015-02-12 13:40:32 -0500 | [diff] [blame] | 239 | } | 
|  | 240 |  | 
|  | 241 | return resp, nil | 
|  | 242 | } | 
| Jamie Hannaford | 647cea5 | 2015-03-23 17:15:07 +0100 | [diff] [blame] | 243 |  | 
|  | 244 | func defaultOkCodes(method string) []int { | 
|  | 245 | switch { | 
|  | 246 | case method == "GET": | 
|  | 247 | return []int{200} | 
|  | 248 | case method == "POST": | 
|  | 249 | return []int{201, 202} | 
|  | 250 | case method == "PUT": | 
|  | 251 | return []int{201, 202} | 
| Krzysztof Kwapisiewicz | 136d2c2 | 2016-02-03 15:36:06 +0100 | [diff] [blame] | 252 | case method == "PATCH": | 
|  | 253 | return []int{200, 204} | 
| Jamie Hannaford | 647cea5 | 2015-03-23 17:15:07 +0100 | [diff] [blame] | 254 | case method == "DELETE": | 
|  | 255 | return []int{202, 204} | 
|  | 256 | } | 
|  | 257 |  | 
|  | 258 | return []int{} | 
|  | 259 | } | 
| Jamie Hannaford | 2a9e74f | 2015-03-24 14:55:24 +0100 | [diff] [blame] | 260 |  | 
|  | 261 | func (client *ProviderClient) Get(url string, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { | 
|  | 262 | if opts == nil { | 
|  | 263 | opts = &RequestOpts{} | 
|  | 264 | } | 
|  | 265 | if JSONResponse != nil { | 
|  | 266 | opts.JSONResponse = JSONResponse | 
|  | 267 | } | 
|  | 268 | return client.Request("GET", url, *opts) | 
|  | 269 | } | 
|  | 270 |  | 
|  | 271 | func (client *ProviderClient) Post(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { | 
|  | 272 | if opts == nil { | 
|  | 273 | opts = &RequestOpts{} | 
|  | 274 | } | 
|  | 275 |  | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 276 | if v, ok := (JSONBody).(io.ReadSeeker); ok { | 
| Jamie Hannaford | 2a9e74f | 2015-03-24 14:55:24 +0100 | [diff] [blame] | 277 | opts.RawBody = v | 
|  | 278 | } else if JSONBody != nil { | 
|  | 279 | opts.JSONBody = JSONBody | 
|  | 280 | } | 
|  | 281 |  | 
|  | 282 | if JSONResponse != nil { | 
|  | 283 | opts.JSONResponse = JSONResponse | 
|  | 284 | } | 
|  | 285 |  | 
|  | 286 | return client.Request("POST", url, *opts) | 
|  | 287 | } | 
|  | 288 |  | 
|  | 289 | func (client *ProviderClient) Put(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { | 
|  | 290 | if opts == nil { | 
|  | 291 | opts = &RequestOpts{} | 
|  | 292 | } | 
|  | 293 |  | 
| Brendan ODonnell | a69b347 | 2015-04-27 13:59:41 -0500 | [diff] [blame] | 294 | if v, ok := (JSONBody).(io.ReadSeeker); ok { | 
| Jamie Hannaford | 2a9e74f | 2015-03-24 14:55:24 +0100 | [diff] [blame] | 295 | opts.RawBody = v | 
|  | 296 | } else if JSONBody != nil { | 
|  | 297 | opts.JSONBody = JSONBody | 
|  | 298 | } | 
|  | 299 |  | 
|  | 300 | if JSONResponse != nil { | 
|  | 301 | opts.JSONResponse = JSONResponse | 
|  | 302 | } | 
|  | 303 |  | 
|  | 304 | return client.Request("PUT", url, *opts) | 
|  | 305 | } | 
|  | 306 |  | 
| Krzysztof Kwapisiewicz | 136d2c2 | 2016-02-03 15:36:06 +0100 | [diff] [blame] | 307 | func (client *ProviderClient) Patch(url string, JSONBody interface{}, JSONResponse *interface{}, opts *RequestOpts) (*http.Response, error) { | 
|  | 308 | if opts == nil { | 
|  | 309 | opts = &RequestOpts{} | 
|  | 310 | } | 
|  | 311 |  | 
|  | 312 | if v, ok := (JSONBody).(io.ReadSeeker); ok { | 
|  | 313 | opts.RawBody = v | 
|  | 314 | } else if JSONBody != nil { | 
|  | 315 | opts.JSONBody = JSONBody | 
|  | 316 | } | 
|  | 317 |  | 
|  | 318 | if JSONResponse != nil { | 
|  | 319 | opts.JSONResponse = JSONResponse | 
|  | 320 | } | 
|  | 321 |  | 
|  | 322 | return client.Request("PATCH", url, *opts) | 
|  | 323 | } | 
|  | 324 |  | 
| Jamie Hannaford | 2a9e74f | 2015-03-24 14:55:24 +0100 | [diff] [blame] | 325 | func (client *ProviderClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { | 
|  | 326 | if opts == nil { | 
|  | 327 | opts = &RequestOpts{} | 
|  | 328 | } | 
|  | 329 |  | 
|  | 330 | return client.Request("DELETE", url, *opts) | 
|  | 331 | } |