blob: 3c65984b40f80188ed12d02a5ce309441f4be682 [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.
14const DefaultUserAgent = "gophercloud/v1.0"
15
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
Ash Wilson89466cc2014-08-29 11:27:39 -040066}
67
Jamie Hannafordb280dea2014-10-24 15:14:06 +020068// AuthenticatedHeaders returns a map of HTTP headers that are common for all
69// authenticated service requests.
Ash Wilson89466cc2014-08-29 11:27:39 -040070func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
Ash Wilson89eec332015-02-12 13:40:32 -050071 if client.TokenID == "" {
72 return map[string]string{}
73 }
Ash Wilson89466cc2014-08-29 11:27:39 -040074 return map[string]string{"X-Auth-Token": client.TokenID}
75}
Ash Wilson89eec332015-02-12 13:40:32 -050076
77// RequestOpts customizes the behavior of the provider.Request() method.
78type RequestOpts struct {
79 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
80 // content type of the request will default to "application/json" unless overridden by MoreHeaders.
81 // It's an error to specify both a JSONBody and a RawBody.
82 JSONBody interface{}
83 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
84 // will be set unless one is provided explicitly by MoreHeaders.
85 RawBody io.Reader
86
87 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
88 // JSON.
Ash Wilson2491b4c2015-02-12 16:13:39 -050089 JSONResponse interface{}
Ash Wilson89eec332015-02-12 13:40:32 -050090 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
91 // the response has a different code, an error will be returned.
92 OkCodes []int
93
94 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
95 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
96 // the default Accept header or an inferred Content-Type, for example.
97 MoreHeaders map[string]string
98}
99
100// UnexpectedResponseCodeError is returned by the Request method when a response code other than
101// those listed in OkCodes is encountered.
102type UnexpectedResponseCodeError struct {
103 URL string
104 Method string
105 Expected []int
106 Actual int
107 Body []byte
108}
109
110func (err *UnexpectedResponseCodeError) Error() string {
111 return fmt.Sprintf(
112 "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
113 err.Expected, err.Method, err.URL, err.Actual, err.Body,
114 )
115}
116
117var applicationJSON = "application/json"
118
119// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
120// header will automatically be provided.
121func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
122 var body io.Reader
123 var contentType *string
124
125 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
126 // io.Reader as-is. Default the content-type to application/json.
Ash Wilson89eec332015-02-12 13:40:32 -0500127 if options.JSONBody != nil {
128 if options.RawBody != nil {
129 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
130 }
131
132 rendered, err := json.Marshal(options.JSONBody)
133 if err != nil {
134 return nil, err
135 }
136
137 body = bytes.NewReader(rendered)
138 contentType = &applicationJSON
139 }
140
141 if options.RawBody != nil {
142 body = options.RawBody
143 }
144
145 // Construct the http.Request.
Ash Wilson89eec332015-02-12 13:40:32 -0500146 req, err := http.NewRequest(method, url, body)
147 if err != nil {
148 return nil, err
149 }
150
151 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
152 // modify or omit any header.
Ash Wilson89eec332015-02-12 13:40:32 -0500153 if contentType != nil {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500154 req.Header.Set("Content-Type", *contentType)
Ash Wilson89eec332015-02-12 13:40:32 -0500155 }
Ash Wilson54d62fa2015-02-12 15:09:46 -0500156 req.Header.Set("Accept", applicationJSON)
Ash Wilson89eec332015-02-12 13:40:32 -0500157
158 for k, v := range client.AuthenticatedHeaders() {
159 req.Header.Add(k, v)
160 }
161
Jon Perrittf0a1fee2015-02-13 12:53:23 -0700162 // Set the User-Agent header
163 req.Header.Set("User-Agent", client.UserAgent.Join())
164
Ash Wilson89eec332015-02-12 13:40:32 -0500165 if options.MoreHeaders != nil {
166 for k, v := range options.MoreHeaders {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500167 fmt.Printf("Applying header [%s: %v]\n", k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500168 if v != "" {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500169 req.Header.Set(k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500170 } else {
171 req.Header.Del(k)
172 }
173 }
174 }
175
Jon Perritt2b5e3e12015-02-13 12:15:08 -0700176 // Issue the request.
Ash Wilson89eec332015-02-12 13:40:32 -0500177 resp, err := client.HTTPClient.Do(req)
178 if err != nil {
179 return nil, err
180 }
181
182 // Validate the response code, if requested to do so.
Ash Wilson89eec332015-02-12 13:40:32 -0500183 if options.OkCodes != nil {
184 var ok bool
185 for _, code := range options.OkCodes {
186 if resp.StatusCode == code {
187 ok = true
188 break
189 }
190 }
191 if !ok {
192 body, _ := ioutil.ReadAll(resp.Body)
193 resp.Body.Close()
194 return resp, &UnexpectedResponseCodeError{
195 URL: url,
196 Method: method,
197 Expected: options.OkCodes,
198 Actual: resp.StatusCode,
199 Body: body,
200 }
201 }
202 }
203
204 // Parse the response body as JSON, if requested to do so.
Ash Wilson89eec332015-02-12 13:40:32 -0500205 if options.JSONResponse != nil {
206 defer resp.Body.Close()
207 json.NewDecoder(resp.Body).Decode(options.JSONResponse)
208 }
209
210 return resp, nil
211}