blob: 092a07822f12aab5983398059c93a253c796eeb4 [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"
10)
11
Jamie Hannafordb280dea2014-10-24 15:14:06 +020012// ProviderClient stores details that are required to interact with any
13// services within a specific provider's API.
Ash Wilson89466cc2014-08-29 11:27:39 -040014//
Jamie Hannafordb280dea2014-10-24 15:14:06 +020015// Generally, you acquire a ProviderClient by calling the NewClient method in
16// the appropriate provider's child package, providing whatever authentication
17// credentials are required.
Ash Wilson89466cc2014-08-29 11:27:39 -040018type ProviderClient struct {
Jamie Hannafordb280dea2014-10-24 15:14:06 +020019 // IdentityBase is the base URL used for a particular provider's identity
20 // service - it will be used when issuing authenticatation requests. It
21 // should point to the root resource of the identity service, not a specific
22 // identity version.
Ash Wilson09694b92014-09-09 14:08:27 -040023 IdentityBase string
24
Jamie Hannafordb280dea2014-10-24 15:14:06 +020025 // IdentityEndpoint is the identity endpoint. This may be a specific version
26 // of the identity service. If this is the case, this endpoint is used rather
27 // than querying versions first.
Ash Wilsonc6372fe2014-09-03 11:24:52 -040028 IdentityEndpoint string
29
Jamie Hannafordb280dea2014-10-24 15:14:06 +020030 // TokenID is the ID of the most recently issued valid token.
Ash Wilson89466cc2014-08-29 11:27:39 -040031 TokenID string
Ash Wilsonb8401a72014-09-08 17:07:49 -040032
Jamie Hannafordb280dea2014-10-24 15:14:06 +020033 // EndpointLocator describes how this provider discovers the endpoints for
34 // its constituent services.
Ash Wilsonb8401a72014-09-08 17:07:49 -040035 EndpointLocator EndpointLocator
Ash Wilson89eec332015-02-12 13:40:32 -050036
37 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
38 HTTPClient http.Client
Ash Wilson89466cc2014-08-29 11:27:39 -040039}
40
Jamie Hannafordb280dea2014-10-24 15:14:06 +020041// AuthenticatedHeaders returns a map of HTTP headers that are common for all
42// authenticated service requests.
Ash Wilson89466cc2014-08-29 11:27:39 -040043func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
Ash Wilson89eec332015-02-12 13:40:32 -050044 if client.TokenID == "" {
45 return map[string]string{}
46 }
Ash Wilson89466cc2014-08-29 11:27:39 -040047 return map[string]string{"X-Auth-Token": client.TokenID}
48}
Ash Wilson89eec332015-02-12 13:40:32 -050049
50// RequestOpts customizes the behavior of the provider.Request() method.
51type RequestOpts struct {
52 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
53 // content type of the request will default to "application/json" unless overridden by MoreHeaders.
54 // It's an error to specify both a JSONBody and a RawBody.
55 JSONBody interface{}
56 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type
57 // will be set unless one is provided explicitly by MoreHeaders.
58 RawBody io.Reader
59
60 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
61 // JSON.
62 JSONResponse *interface{}
63 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
64 // the response has a different code, an error will be returned.
65 OkCodes []int
66
67 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
68 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress
69 // the default Accept header or an inferred Content-Type, for example.
70 MoreHeaders map[string]string
71}
72
73// UnexpectedResponseCodeError is returned by the Request method when a response code other than
74// those listed in OkCodes is encountered.
75type UnexpectedResponseCodeError struct {
76 URL string
77 Method string
78 Expected []int
79 Actual int
80 Body []byte
81}
82
83func (err *UnexpectedResponseCodeError) Error() string {
84 return fmt.Sprintf(
85 "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
86 err.Expected, err.Method, err.URL, err.Actual, err.Body,
87 )
88}
89
90var applicationJSON = "application/json"
91
92// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
93// header will automatically be provided.
94func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
95 var body io.Reader
96 var contentType *string
97
98 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
99 // io.Reader as-is. Default the content-type to application/json.
100
101 if options.JSONBody != nil {
102 if options.RawBody != nil {
103 panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
104 }
105
106 rendered, err := json.Marshal(options.JSONBody)
107 if err != nil {
108 return nil, err
109 }
110
111 body = bytes.NewReader(rendered)
112 contentType = &applicationJSON
113 }
114
115 if options.RawBody != nil {
116 body = options.RawBody
117 }
118
119 // Construct the http.Request.
120
121 req, err := http.NewRequest(method, url, body)
122 if err != nil {
123 return nil, err
124 }
125
126 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
127 // modify or omit any header.
128
129 if contentType != nil {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500130 req.Header.Set("Content-Type", *contentType)
Ash Wilson89eec332015-02-12 13:40:32 -0500131 }
Ash Wilson54d62fa2015-02-12 15:09:46 -0500132 req.Header.Set("Accept", applicationJSON)
Ash Wilson89eec332015-02-12 13:40:32 -0500133
134 for k, v := range client.AuthenticatedHeaders() {
135 req.Header.Add(k, v)
136 }
137
138 if options.MoreHeaders != nil {
139 for k, v := range options.MoreHeaders {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500140 fmt.Printf("Applying header [%s: %v]\n", k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500141 if v != "" {
Ash Wilson54d62fa2015-02-12 15:09:46 -0500142 req.Header.Set(k, v)
Ash Wilson89eec332015-02-12 13:40:32 -0500143 } else {
144 req.Header.Del(k)
145 }
146 }
147 }
148
149 // Issue the request.
150
151 resp, err := client.HTTPClient.Do(req)
152 if err != nil {
153 return nil, err
154 }
155
156 // Validate the response code, if requested to do so.
157
158 if options.OkCodes != nil {
159 var ok bool
160 for _, code := range options.OkCodes {
161 if resp.StatusCode == code {
162 ok = true
163 break
164 }
165 }
166 if !ok {
167 body, _ := ioutil.ReadAll(resp.Body)
168 resp.Body.Close()
169 return resp, &UnexpectedResponseCodeError{
170 URL: url,
171 Method: method,
172 Expected: options.OkCodes,
173 Actual: resp.StatusCode,
174 Body: body,
175 }
176 }
177 }
178
179 // Parse the response body as JSON, if requested to do so.
180
181 if options.JSONResponse != nil {
182 defer resp.Body.Close()
183 json.NewDecoder(resp.Body).Decode(options.JSONResponse)
184 }
185
186 return resp, nil
187}