You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/vendor/github.com/softlayer/softlayer-go/session/rest.go

315 lines
7.4 KiB

/**
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package session
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"time"
"github.com/softlayer/softlayer-go/datatypes"
"github.com/softlayer/softlayer-go/sl"
)
type RestTransport struct{}
// DoRequest - Implementation of the TransportHandler interface for handling
// calls to the REST endpoint.
func (r *RestTransport) DoRequest(sess *Session, service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error {
restMethod := httpMethod(method, args)
// Parse any method parameters and determine the HTTP method
var parameters []byte
if len(args) > 0 {
// parse the parameters
parameters, _ = json.Marshal(
map[string]interface{}{
"parameters": args,
})
}
path := buildPath(service, method, options)
resp, code, err := sendHTTPRequest(
sess,
path,
restMethod,
bytes.NewBuffer(parameters),
options)
if err != nil {
//Preserve the original sl error
if _, ok := err.(sl.Error); ok {
return err
}
return sl.Error{Wrapped: err, StatusCode: code}
}
err = findResponseError(code, resp)
if err != nil {
return err
}
// Some APIs that normally return a collection, omit the []'s when the API returns a single value
returnType := reflect.TypeOf(pResult).String()
if strings.Index(returnType, "[]") == 1 && strings.Index(string(resp), "[") != 0 {
resp = []byte("[" + string(resp) + "]")
}
// At this point, all that's left to do is parse the return value to the appropriate type, and return
// any parse errors (or nil if successful)
err = nil
switch pResult.(type) {
case *[]uint8:
// exclude quotes
*pResult.(*[]uint8) = resp[1 : len(resp)-1]
case *datatypes.Void:
case *uint:
var val uint64
val, err = strconv.ParseUint(string(resp), 0, 64)
if err == nil {
*pResult.(*uint) = uint(val)
}
case *bool:
*pResult.(*bool), err = strconv.ParseBool(string(resp))
case *string:
str := string(resp)
strIdx := len(str) - 1
if str == "null" {
str = ""
} else if str[0] == '"' && str[strIdx] == '"' {
rawStr := rawString{str}
err = json.Unmarshal([]byte(`{"val":`+str+`}`), &rawStr)
if err == nil {
str = rawStr.Val
}
}
*pResult.(*string) = str
default:
// Must be a json representation of one of the many softlayer datatypes
err = json.Unmarshal(resp, pResult)
}
if err != nil {
err = sl.Error{Message: err.Error(), Wrapped: err}
}
return err
}
type rawString struct {
Val string
}
func buildPath(service string, method string, options *sl.Options) string {
path := service
if options.Id != nil {
path = path + "/" + strconv.Itoa(*options.Id)
}
// omit the API method name if the method represents one of the basic REST methods
if method != "getObject" && method != "deleteObject" && method != "createObject" &&
method != "editObject" && method != "editObjects" {
path = path + "/" + method
}
return path + ".json"
}
func encodeQuery(opts *sl.Options) string {
query := new(url.URL).Query()
if opts.Mask != "" {
query.Add("objectMask", opts.Mask)
}
if opts.Filter != "" {
query.Add("objectFilter", opts.Filter)
}
// resultLimit=<offset>,<limit>
// If offset unspecified, default to 0
if opts.Limit != nil {
startOffset := 0
if opts.Offset != nil {
startOffset = *opts.Offset
}
query.Add("resultLimit", fmt.Sprintf("%d,%d", startOffset, *opts.Limit))
}
return query.Encode()
}
func sendHTTPRequest(
sess *Session, path string, requestType string,
requestBody *bytes.Buffer, options *sl.Options) ([]byte, int, error) {
retries := sess.Retries
if retries < 2 {
return makeHTTPRequest(sess, path, requestType, requestBody, options)
}
wait := sess.RetryWait
if wait == 0 {
wait = DefaultRetryWait
}
return tryHTTPRequest(retries, wait, sess, path, requestType, requestBody, options)
}
func tryHTTPRequest(
retries int, wait time.Duration, sess *Session,
path string, requestType string, requestBody *bytes.Buffer,
options *sl.Options) ([]byte, int, error) {
resp, code, err := makeHTTPRequest(sess, path, requestType, requestBody, options)
if err != nil {
if !isRetryable(err) {
return resp, code, err
}
if retries--; retries > 0 {
jitter := time.Duration(rand.Int63n(int64(wait)))
wait = wait + jitter/2
time.Sleep(wait)
return tryHTTPRequest(
retries, wait, sess, path, requestType, requestBody, options)
}
}
return resp, code, err
}
func makeHTTPRequest(
session *Session, path string, requestType string,
requestBody *bytes.Buffer, options *sl.Options) ([]byte, int, error) {
log := Logger
client := session.HTTPClient
if client == nil {
client = &http.Client{}
}
client.Timeout = DefaultTimeout
if session.Timeout != 0 {
client.Timeout = session.Timeout
}
var url string
if session.Endpoint == "" {
url = url + DefaultEndpoint
} else {
url = url + session.Endpoint
}
url = fmt.Sprintf("%s/%s", strings.TrimRight(url, "/"), path)
req, err := http.NewRequest(requestType, url, requestBody)
if err != nil {
return nil, 0, err
}
if session.APIKey != "" {
req.SetBasicAuth(session.UserName, session.APIKey)
} else if session.AuthToken != "" {
req.SetBasicAuth(fmt.Sprintf("%d", session.UserId), session.AuthToken)
}
// For cases where session is built from the raw structure and not using New() , the UserAgent would be empty
if session.userAgent == "" {
session.userAgent = getDefaultUserAgent()
}
req.Header.Set("User-Agent", session.userAgent)
if session.Headers != nil {
for key, value := range session.Headers {
req.Header.Set(key, value)
}
}
req.URL.RawQuery = encodeQuery(options)
if session.Debug {
log.Println("[DEBUG] Request URL: ", requestType, req.URL)
log.Println("[DEBUG] Parameters: ", requestBody.String())
}
resp, err := client.Do(req)
if err != nil {
statusCode := 520
if resp != nil && resp.StatusCode != 0 {
statusCode = resp.StatusCode
}
if isTimeout(err) {
statusCode = 599
}
return nil, statusCode, err
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
if session.Debug {
log.Println("[DEBUG] Status Code: ", resp.StatusCode)
log.Println("[DEBUG] Response: ", string(responseBody))
}
err = findResponseError(resp.StatusCode, responseBody)
return responseBody, resp.StatusCode, err
}
func httpMethod(name string, args []interface{}) string {
if name == "deleteObject" {
return "DELETE"
} else if name == "editObject" || name == "editObjects" {
return "PUT"
} else if name == "createObject" || name == "createObjects" || len(args) > 0 {
return "POST"
}
return "GET"
}
func findResponseError(code int, resp []byte) error {
if code < 200 || code > 299 {
e := sl.Error{StatusCode: code}
err := json.Unmarshal(resp, &e)
// If unparseable, wrap the json error
if err != nil {
e.Wrapped = err
e.Message = err.Error()
}
return e
}
return nil
}