lego/vendor/github.com/exoscale/egoscale/request.go

357 lines
8.4 KiB
Go
Raw Normal View History

package egoscale
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
// Error formats a CloudStack error into a standard error
func (e *ErrorResponse) Error() string {
return fmt.Sprintf("API error %s %d (%s %d): %s", e.ErrorCode, e.ErrorCode, e.CSErrorCode, e.CSErrorCode, e.ErrorText)
}
// Success computes the values based on the RawMessage, either string or bool
func (e *booleanResponse) IsSuccess() (bool, error) {
if e.Success == nil {
return false, fmt.Errorf("Not a valid booleanResponse")
}
str := ""
if err := json.Unmarshal(e.Success, &str); err != nil {
boolean := false
if e := json.Unmarshal(e.Success, &boolean); e != nil {
return false, e
}
return boolean, nil
}
return str == "true", nil
}
// Error formats a CloudStack job response into a standard error
func (e *booleanResponse) Error() error {
success, err := e.IsSuccess()
if err != nil {
return err
}
if success {
return nil
}
fmt.Printf("%#v", e)
return fmt.Errorf("API error: %s", e.DisplayText)
}
func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
a, _ := rawValues(b)
if a == nil {
b, err = rawValue(b)
if err != nil {
return nil, err
}
}
if resp.StatusCode >= 400 {
errorResponse := new(ErrorResponse)
if e := json.Unmarshal(b, errorResponse); e == nil && errorResponse.ErrorCode > 0 {
return nil, errorResponse
}
return nil, fmt.Errorf("%d %s", resp.StatusCode, b)
}
return b, nil
}
// asyncRequest perform an asynchronous job with a context
func (exo *Client) asyncRequest(ctx context.Context, request AsyncCommand) (interface{}, error) {
var err error
res := request.asyncResponse()
exo.AsyncRequestWithContext(ctx, request, func(j *AsyncJobResult, er error) bool {
if er != nil {
err = er
return false
}
if j.JobStatus == Success {
if r := j.Response(res); err != nil {
err = r
}
return false
}
return true
})
return res, err
}
// syncRequest performs a sync request with a context
func (exo *Client) syncRequest(ctx context.Context, request syncCommand) (interface{}, error) {
body, err := exo.request(ctx, request)
if err != nil {
return nil, err
}
response := request.response()
err = json.Unmarshal(body, response)
// booleanResponse will alway be valid...
if err == nil {
if br, ok := response.(*booleanResponse); ok {
success, e := br.IsSuccess()
if e != nil {
return nil, e
}
if !success {
err = fmt.Errorf("Not a valid booleanResponse")
}
}
}
if err != nil {
errResponse := new(ErrorResponse)
if e := json.Unmarshal(body, errResponse); e == nil && errResponse.ErrorCode > 0 {
return errResponse, nil
}
return nil, err
}
return response, nil
}
// BooleanRequest performs the given boolean command
func (exo *Client) BooleanRequest(req Command) error {
resp, err := exo.Request(req)
if err != nil {
return err
}
if b, ok := resp.(*booleanResponse); ok {
return b.Error()
}
panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.name(), resp))
}
// BooleanRequestWithContext performs the given boolean command
func (exo *Client) BooleanRequestWithContext(ctx context.Context, req Command) error {
resp, err := exo.RequestWithContext(ctx, req)
if err != nil {
return err
}
if b, ok := resp.(*booleanResponse); ok {
return b.Error()
}
panic(fmt.Errorf("The command %s is not a proper boolean response. %#v", req.name(), resp))
}
// Request performs the given command
func (exo *Client) Request(request Command) (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), exo.Timeout)
defer cancel()
switch request.(type) {
case syncCommand:
return exo.syncRequest(ctx, request.(syncCommand))
case AsyncCommand:
return exo.asyncRequest(ctx, request.(AsyncCommand))
default:
panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.name()))
}
}
// RequestWithContext preforms a request with a context
func (exo *Client) RequestWithContext(ctx context.Context, request Command) (interface{}, error) {
switch request.(type) {
case syncCommand:
return exo.syncRequest(ctx, request.(syncCommand))
case AsyncCommand:
return exo.asyncRequest(ctx, request.(AsyncCommand))
default:
panic(fmt.Errorf("The command %s is not a proper Sync or Async command", request.name()))
}
}
// AsyncRequest performs the given command
func (exo *Client) AsyncRequest(request AsyncCommand, callback WaitAsyncJobResultFunc) {
ctx, cancel := context.WithTimeout(context.Background(), exo.Timeout)
defer cancel()
exo.AsyncRequestWithContext(ctx, request, callback)
}
// AsyncRequestWithContext preforms a request with a context
func (exo *Client) AsyncRequestWithContext(ctx context.Context, request AsyncCommand, callback WaitAsyncJobResultFunc) {
body, err := exo.request(ctx, request)
if err != nil {
callback(nil, err)
return
}
jobResult := new(AsyncJobResult)
if err := json.Unmarshal(body, jobResult); err != nil {
r := new(ErrorResponse)
if e := json.Unmarshal(body, r); e != nil && r.ErrorCode > 0 {
if !callback(nil, r) {
return
}
}
if !callback(nil, err) {
return
}
}
// Successful response
if jobResult.JobID == "" || jobResult.JobStatus != Pending {
callback(jobResult, nil)
// without a JobID, the next requests will only fail
return
}
for iteration := 0; ; iteration++ {
time.Sleep(exo.RetryStrategy(int64(iteration)))
req := &QueryAsyncJobResult{JobID: jobResult.JobID}
resp, err := exo.syncRequest(ctx, req)
if err != nil && !callback(nil, err) {
return
}
result, ok := resp.(*QueryAsyncJobResultResponse)
if !ok && !callback(nil, fmt.Errorf("AsyncJobResult expected, got %t", resp)) {
return
}
res := (*AsyncJobResult)(result)
if res.JobStatus == Failure {
if !callback(nil, res.Error()) {
return
}
} else {
if !callback(res, nil) {
return
}
}
}
}
// Payload builds the HTTP request from the given command
func (exo *Client) Payload(request Command) (string, error) {
params := url.Values{}
err := prepareValues("", &params, request)
if err != nil {
return "", err
}
if hookReq, ok := request.(onBeforeHook); ok {
if err := hookReq.onBeforeSend(&params); err != nil {
return "", err
}
}
params.Set("apikey", exo.APIKey)
params.Set("command", request.name())
params.Set("response", "json")
// This code is borrowed from net/url/url.go
// The way it's encoded by net/url doesn't match
// how CloudStack works.
var buf bytes.Buffer
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
prefix := csEncode(k) + "="
for _, v := range params[k] {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(prefix)
buf.WriteString(csEncode(v))
}
}
return buf.String(), nil
}
// Sign signs the HTTP request and return it
func (exo *Client) Sign(query string) (string, error) {
mac := hmac.New(sha1.New, []byte(exo.apiSecret))
_, err := mac.Write([]byte(strings.ToLower(query)))
if err != nil {
return "", err
}
signature := csEncode(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
return fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature), nil
}
// request makes a Request while being close to the metal
func (exo *Client) request(ctx context.Context, req Command) (json.RawMessage, error) {
payload, err := exo.Payload(req)
if err != nil {
return nil, err
}
query, err := exo.Sign(payload)
if err != nil {
return nil, err
}
method := "GET"
url := fmt.Sprintf("%s?%s", exo.Endpoint, query)
var body io.Reader
// respect Internet Explorer limit of 2048
if len(url) > 1<<11 {
url = exo.Endpoint
method = "POST"
body = strings.NewReader(query)
}
request, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
request = request.WithContext(ctx)
request.Header.Add("User-Agent", fmt.Sprintf("exoscale/egoscale (%v)", Version))
if method == "POST" {
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request.Header.Add("Content-Length", strconv.Itoa(len(query)))
}
resp, err := exo.HTTPClient.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close() // nolint: errcheck
text, err := exo.parseResponse(resp)
if err != nil {
return nil, err
}
return text, nil
}