blob: 331673321b2142c2388b21251de94493559781aa [file] [log] [blame]
package gophercloud
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
)
// ProviderClient stores details that are required to interact with any
// services within a specific provider's API.
//
// Generally, you acquire a ProviderClient by calling the NewClient method in
// the appropriate provider's child package, providing whatever authentication
// credentials are required.
type ProviderClient struct {
// IdentityBase is the base URL used for a particular provider's identity
// service - it will be used when issuing authenticatation requests. It
// should point to the root resource of the identity service, not a specific
// identity version.
IdentityBase string
// IdentityEndpoint is the identity endpoint. This may be a specific version
// of the identity service. If this is the case, this endpoint is used rather
// than querying versions first.
IdentityEndpoint string
// TokenID is the ID of the most recently issued valid token.
TokenID string
// EndpointLocator describes how this provider discovers the endpoints for
// its constituent services.
EndpointLocator EndpointLocator
// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
HTTPClient http.Client
}
// AuthenticatedHeaders returns a map of HTTP headers that are common for all
// authenticated service requests.
func (client *ProviderClient) AuthenticatedHeaders() map[string]string {
if client.TokenID == "" {
return map[string]string{}
}
return map[string]string{"X-Auth-Token": client.TokenID}
}
// RequestOpts customizes the behavior of the provider.Request() method.
type RequestOpts struct {
// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
// content type of the request will default to "application/json" unless overridden by MoreHeaders.
// It's an error to specify both a JSONBody and a RawBody.
JSONBody interface{}
// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
// will be set unless one is provided explicitly by MoreHeaders.
RawBody io.Reader
// JSONResponse, if provided, will be populated with the contents of the response body parsed as
// JSON.
JSONResponse interface{}
// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
// the response has a different code, an error will be returned.
OkCodes []int
// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
// the default Accept header or an inferred Content-Type, for example.
MoreHeaders map[string]string
}
// UnexpectedResponseCodeError is returned by the Request method when a response code other than
// those listed in OkCodes is encountered.
type UnexpectedResponseCodeError struct {
URL string
Method string
Expected []int
Actual int
Body []byte
}
func (err *UnexpectedResponseCodeError) Error() string {
return fmt.Sprintf(
"Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s",
err.Expected, err.Method, err.URL, err.Actual, err.Body,
)
}
var applicationJSON = "application/json"
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options RequestOpts) (*http.Response, error) {
var body io.Reader
var contentType *string
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
// io.Reader as-is. Default the content-type to application/json.
if options.JSONBody != nil {
if options.RawBody != nil {
panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
}
rendered, err := json.Marshal(options.JSONBody)
if err != nil {
return nil, err
}
body = bytes.NewReader(rendered)
contentType = &applicationJSON
}
if options.RawBody != nil {
body = options.RawBody
}
// Construct the http.Request.
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
// modify or omit any header.
if contentType != nil {
req.Header.Set("Content-Type", *contentType)
}
req.Header.Set("Accept", applicationJSON)
for k, v := range client.AuthenticatedHeaders() {
req.Header.Add(k, v)
}
if options.MoreHeaders != nil {
for k, v := range options.MoreHeaders {
fmt.Printf("Applying header [%s: %v]\n", k, v)
if v != "" {
req.Header.Set(k, v)
} else {
req.Header.Del(k)
}
}
}
// Issue the request.
resp, err := client.HTTPClient.Do(req)
if err != nil {
return nil, err
}
// Validate the response code, if requested to do so.
if options.OkCodes != nil {
var ok bool
for _, code := range options.OkCodes {
if resp.StatusCode == code {
ok = true
break
}
}
if !ok {
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
return resp, &UnexpectedResponseCodeError{
URL: url,
Method: method,
Expected: options.OkCodes,
Actual: resp.StatusCode,
Body: body,
}
}
}
// Parse the response body as JSON, if requested to do so.
if options.JSONResponse != nil {
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(options.JSONResponse)
}
return resp, nil
}