Update Lego (Gandi API v5, cloudxns, ...)
This commit is contained in:
parent
dd873fbeee
commit
7d3dd5a0e4
43 changed files with 4112 additions and 1238 deletions
511
vendor/github.com/exoscale/egoscale/request.go
generated
vendored
511
vendor/github.com/exoscale/egoscale/request.go
generated
vendored
|
@ -1,20 +1,164 @@
|
|||
package egoscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Command represent a CloudStack request
|
||||
type Command interface {
|
||||
// CloudStack API command name
|
||||
name() string
|
||||
// Response interface to Unmarshal the JSON into
|
||||
response() interface{}
|
||||
}
|
||||
|
||||
// AsyncCommand represents a async CloudStack request
|
||||
type AsyncCommand interface {
|
||||
// CloudStack API command name
|
||||
name() string
|
||||
// Response interface to Unmarshal the JSON into
|
||||
asyncResponse() interface{}
|
||||
}
|
||||
|
||||
// Command represents an action to be done on the params before sending them
|
||||
//
|
||||
// This little took helps with issue of relying on JSON serialization logic only.
|
||||
// `omitempty` may make sense in some cases but not all the time.
|
||||
type onBeforeHook interface {
|
||||
onBeforeSend(params *url.Values) error
|
||||
}
|
||||
|
||||
const (
|
||||
// Pending represents a job in progress
|
||||
Pending JobStatusType = iota
|
||||
// Success represents a successfully completed job
|
||||
Success
|
||||
// Failure represents a job that has failed to complete
|
||||
Failure
|
||||
)
|
||||
|
||||
// JobStatusType represents the status of a Job
|
||||
type JobStatusType int
|
||||
|
||||
const (
|
||||
// Unauthorized represents ... (TODO)
|
||||
Unauthorized ErrorCode = 401
|
||||
// MethodNotAllowed represents ... (TODO)
|
||||
MethodNotAllowed = 405
|
||||
// UnsupportedActionError represents ... (TODO)
|
||||
UnsupportedActionError = 422
|
||||
// APILimitExceeded represents ... (TODO)
|
||||
APILimitExceeded = 429
|
||||
// MalformedParameterError represents ... (TODO)
|
||||
MalformedParameterError = 430
|
||||
// ParamError represents ... (TODO)
|
||||
ParamError = 431
|
||||
|
||||
// InternalError represents a server error
|
||||
InternalError = 530
|
||||
// AccountError represents ... (TODO)
|
||||
AccountError = 531
|
||||
// AccountResourceLimitError represents ... (TODO)
|
||||
AccountResourceLimitError = 532
|
||||
// InsufficientCapacityError represents ... (TODO)
|
||||
InsufficientCapacityError = 533
|
||||
// ResourceUnavailableError represents ... (TODO)
|
||||
ResourceUnavailableError = 534
|
||||
// ResourceAllocationError represents ... (TODO)
|
||||
ResourceAllocationError = 535
|
||||
// ResourceInUseError represents ... (TODO)
|
||||
ResourceInUseError = 536
|
||||
// NetworkRuleConflictError represents ... (TODO)
|
||||
NetworkRuleConflictError = 537
|
||||
)
|
||||
|
||||
// ErrorCode represents the CloudStack ApiErrorCode enum
|
||||
//
|
||||
// See: https://github.com/apache/cloudstack/blob/master/api/src/org/apache/cloudstack/api/ApiErrorCode.java
|
||||
type ErrorCode int
|
||||
|
||||
// JobResultResponse represents a generic response to a job task
|
||||
type JobResultResponse struct {
|
||||
AccountID string `json:"accountid,omitempty"`
|
||||
Cmd string `json:"cmd"`
|
||||
Created string `json:"created"`
|
||||
JobID string `json:"jobid"`
|
||||
JobProcStatus int `json:"jobprocstatus"`
|
||||
JobResult *json.RawMessage `json:"jobresult"`
|
||||
JobStatus JobStatusType `json:"jobstatus"`
|
||||
JobResultType string `json:"jobresulttype"`
|
||||
UserID string `json:"userid,omitempty"`
|
||||
}
|
||||
|
||||
// ErrorResponse represents the standard error response from CloudStack
|
||||
type ErrorResponse struct {
|
||||
ErrorCode ErrorCode `json:"errorcode"`
|
||||
CsErrorCode int `json:"cserrorcode"`
|
||||
ErrorText string `json:"errortext"`
|
||||
UUIDList []string `json:"uuidList,omitempty"` // uuid*L*ist is not a typo
|
||||
}
|
||||
|
||||
// Error formats a CloudStack error into a standard error
|
||||
func (e *ErrorResponse) Error() string {
|
||||
return fmt.Sprintf("API error %d (internal code: %d): %s", e.ErrorCode, e.CsErrorCode, e.ErrorText)
|
||||
}
|
||||
|
||||
// booleanAsyncResponse represents a boolean response (usually after a deletion)
|
||||
type booleanAsyncResponse struct {
|
||||
Success bool `json:"success"`
|
||||
DisplayText string `json:"diplaytext,omitempty"`
|
||||
}
|
||||
|
||||
// Error formats a CloudStack job response into a standard error
|
||||
func (e *booleanAsyncResponse) Error() error {
|
||||
if e.Success {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("API error: %s", e.DisplayText)
|
||||
}
|
||||
|
||||
// booleanAsyncResponse represents a boolean response for sync calls
|
||||
type booleanSyncResponse struct {
|
||||
Success string `json:"success"`
|
||||
DisplayText string `json:"displaytext,omitempty"`
|
||||
}
|
||||
|
||||
func (e *booleanSyncResponse) Error() error {
|
||||
if e.Success == "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("API error: %s", e.DisplayText)
|
||||
}
|
||||
|
||||
// AsyncInfo represents the details for any async call
|
||||
//
|
||||
// It retries at most Retries time and waits for Delay between each retry
|
||||
type AsyncInfo struct {
|
||||
Retries int
|
||||
Delay int
|
||||
}
|
||||
|
||||
func csQuotePlus(s string) string {
|
||||
return strings.Replace(s, "+", "%20", -1)
|
||||
s = strings.Replace(s, "+", "%20", -1)
|
||||
s = strings.Replace(s, "%5B", "[", -1)
|
||||
s = strings.Replace(s, "%5D", "]", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
func csEncode(s string) string {
|
||||
|
@ -43,7 +187,7 @@ func rawValues(b json.RawMessage) (json.RawMessage, error) {
|
|||
return i[0], nil
|
||||
}
|
||||
|
||||
func (exo *Client) ParseResponse(resp *http.Response) (json.RawMessage, error) {
|
||||
func (exo *Client) parseResponse(resp *http.Response) (json.RawMessage, error) {
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -59,75 +203,362 @@ func (exo *Client) ParseResponse(resp *http.Response) (json.RawMessage, error) {
|
|||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
var e Error
|
||||
var e ErrorResponse
|
||||
if err := json.Unmarshal(b, &e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
/* Need to account for differet error types */
|
||||
if e.ErrorCode != 0 {
|
||||
return nil, e.Error()
|
||||
} else {
|
||||
var de DNSError
|
||||
if err := json.Unmarshal(b, &de); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("Exoscale error (%d): %s", resp.StatusCode, strings.Join(de.Name, "\n"))
|
||||
}
|
||||
return b, &e
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (exo *Client) Request(command string, params url.Values) (json.RawMessage, error) {
|
||||
// AsyncRequest performs an asynchronous request and polls it for retries * day [s]
|
||||
func (exo *Client) AsyncRequest(req AsyncCommand, async AsyncInfo) (interface{}, error) {
|
||||
body, err := exo.request(req.name(), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(exo.apiSecret))
|
||||
keys := make([]string, 0)
|
||||
unencoded := make([]string, 0)
|
||||
// Is it a Job?
|
||||
job := new(JobResultResponse)
|
||||
if err := json.Unmarshal(body, &job); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Error response
|
||||
errorResponse := new(ErrorResponse)
|
||||
// Successful response
|
||||
resp := req.asyncResponse()
|
||||
if job.JobID == "" || job.JobStatus != Pending {
|
||||
if err := json.Unmarshal(*job.JobResult, resp); err != nil {
|
||||
return job, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// we've got a pending job
|
||||
result := &QueryAsyncJobResultResponse{
|
||||
JobStatus: job.JobStatus,
|
||||
}
|
||||
for async.Retries > 0 && result.JobStatus == Pending {
|
||||
time.Sleep(time.Duration(async.Delay) * time.Second)
|
||||
|
||||
async.Retries--
|
||||
|
||||
req := &QueryAsyncJobResult{JobID: job.JobID}
|
||||
resp, err := exo.Request(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = resp.(*QueryAsyncJobResultResponse)
|
||||
}
|
||||
|
||||
if result.JobStatus == Failure {
|
||||
if err := json.Unmarshal(*result.JobResult, &errorResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return errorResponse, errorResponse
|
||||
}
|
||||
|
||||
if result.JobStatus == Pending {
|
||||
return result, fmt.Errorf("Maximum number of retries reached")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(*result.JobResult, resp); err != nil {
|
||||
if err := json.Unmarshal(*result.JobResult, errorResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return errorResponse, errorResponse
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// BooleanRequest performs a sync request on a boolean call
|
||||
func (exo *Client) BooleanRequest(req Command) error {
|
||||
resp, err := exo.Request(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resp.(*booleanSyncResponse).Error()
|
||||
}
|
||||
|
||||
// BooleanAsyncRequest performs a sync request on a boolean call
|
||||
func (exo *Client) BooleanAsyncRequest(req AsyncCommand, async AsyncInfo) error {
|
||||
resp, err := exo.AsyncRequest(req, async)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resp.(*booleanAsyncResponse).Error()
|
||||
}
|
||||
|
||||
// Request performs a sync request on a generic command
|
||||
func (exo *Client) Request(req Command) (interface{}, error) {
|
||||
body, err := exo.request(req.name(), req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := req.response()
|
||||
if err := json.Unmarshal(body, resp); err != nil {
|
||||
r := new(ErrorResponse)
|
||||
if e := json.Unmarshal(body, r); e != nil {
|
||||
return nil, r
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// request makes a Request while being close to the metal
|
||||
func (exo *Client) request(command string, req interface{}) (json.RawMessage, error) {
|
||||
params := url.Values{}
|
||||
err := prepareValues("", ¶ms, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hookReq, ok := req.(onBeforeHook); ok {
|
||||
hookReq.onBeforeSend(¶ms)
|
||||
}
|
||||
params.Set("apikey", exo.apiKey)
|
||||
params.Set("command", command)
|
||||
params.Set("response", "json")
|
||||
|
||||
for k, _ := range params {
|
||||
// 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 {
|
||||
arg := k + "=" + csEncode(params[k][0])
|
||||
unencoded = append(unencoded, arg)
|
||||
prefix := csEncode(k) + "="
|
||||
for _, v := range params[k] {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte('&')
|
||||
}
|
||||
buf.WriteString(prefix)
|
||||
buf.WriteString(csEncode(v))
|
||||
}
|
||||
}
|
||||
sign_string := strings.ToLower(strings.Join(unencoded, "&"))
|
||||
|
||||
mac.Write([]byte(sign_string))
|
||||
query := buf.String()
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(exo.apiSecret))
|
||||
mac.Write([]byte(strings.ToLower(query)))
|
||||
signature := csEncode(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
|
||||
query := params.Encode()
|
||||
url := exo.endpoint + "?" + csQuotePlus(query) + "&signature=" + signature
|
||||
|
||||
resp, err := exo.client.Get(url)
|
||||
reader := strings.NewReader(fmt.Sprintf("%s&signature=%s", csQuotePlus(query), signature))
|
||||
resp, err := exo.client.Post(exo.endpoint, "application/x-www-form-urlencoded", reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
return exo.ParseResponse(resp)
|
||||
}
|
||||
|
||||
func (exo *Client) DetailedRequest(uri string, params string, method string, header http.Header) (json.RawMessage, error) {
|
||||
url := exo.endpoint + uri
|
||||
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(params))
|
||||
body, err := exo.parseResponse(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header = header
|
||||
return body, nil
|
||||
}
|
||||
|
||||
response, err := exo.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// prepareValues uses a command to build a POST request
|
||||
//
|
||||
// command is not a Command so it's easier to Test
|
||||
func prepareValues(prefix string, params *url.Values, command interface{}) error {
|
||||
value := reflect.ValueOf(command)
|
||||
typeof := reflect.TypeOf(command)
|
||||
// Going up the pointer chain to find the underlying struct
|
||||
for typeof.Kind() == reflect.Ptr {
|
||||
typeof = typeof.Elem()
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
return exo.ParseResponse(response)
|
||||
for i := 0; i < typeof.NumField(); i++ {
|
||||
field := typeof.Field(i)
|
||||
val := value.Field(i)
|
||||
tag := field.Tag
|
||||
if json, ok := tag.Lookup("json"); ok {
|
||||
n, required := extractJSONTag(field.Name, json)
|
||||
name := prefix + n
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
v := val.Int()
|
||||
if v == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, strconv.FormatInt(v, 10))
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
v := val.Uint()
|
||||
if v == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, strconv.FormatUint(v, 10))
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
v := val.Float()
|
||||
if v == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got 0", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, strconv.FormatFloat(v, 'f', -1, 64))
|
||||
}
|
||||
case reflect.String:
|
||||
v := val.String()
|
||||
if v == "" {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got \"\"", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, v)
|
||||
}
|
||||
case reflect.Bool:
|
||||
v := val.Bool()
|
||||
if v == false {
|
||||
if required {
|
||||
params.Set(name, "false")
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, "true")
|
||||
}
|
||||
case reflect.Slice:
|
||||
switch field.Type.Elem().Kind() {
|
||||
case reflect.Uint8:
|
||||
switch field.Type {
|
||||
case reflect.TypeOf(net.IPv4zero):
|
||||
ip := (net.IP)(val.Bytes())
|
||||
if ip == nil || ip.Equal(net.IPv4zero) {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got zero IPv4 address", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
(*params).Set(name, ip.String())
|
||||
}
|
||||
default:
|
||||
if val.Len() == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
v := val.Bytes()
|
||||
(*params).Set(name, base64.StdEncoding.EncodeToString(v))
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
{
|
||||
if val.Len() == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
elems := make([]string, 0, val.Len())
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
// XXX what if the value contains a comma? Double encode?
|
||||
s := val.Index(i).String()
|
||||
elems = append(elems, s)
|
||||
}
|
||||
(*params).Set(name, strings.Join(elems, ","))
|
||||
}
|
||||
}
|
||||
default:
|
||||
if val.Len() == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got empty slice", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
err := prepareList(name, params, val.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Map:
|
||||
if val.Len() == 0 {
|
||||
if required {
|
||||
return fmt.Errorf("%s.%s (%v) is required, got empty map", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
} else {
|
||||
err := prepareMap(name, params, val.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
if required {
|
||||
return fmt.Errorf("Unsupported type %s.%s (%v)", typeof.Name(), field.Name, val.Kind())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[SKIP] %s.%s no json label found", typeof.Name(), field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareList(prefix string, params *url.Values, slice interface{}) error {
|
||||
value := reflect.ValueOf(slice)
|
||||
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
prepareValues(fmt.Sprintf("%s[%d].", prefix, i), params, value.Index(i).Interface())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareMap(prefix string, params *url.Values, m interface{}) error {
|
||||
value := reflect.ValueOf(m)
|
||||
|
||||
for i, key := range value.MapKeys() {
|
||||
var keyName string
|
||||
var keyValue string
|
||||
|
||||
switch key.Kind() {
|
||||
case reflect.String:
|
||||
keyName = key.String()
|
||||
default:
|
||||
return fmt.Errorf("Only map[string]string are supported (XXX)")
|
||||
}
|
||||
|
||||
val := value.MapIndex(key)
|
||||
switch val.Kind() {
|
||||
case reflect.String:
|
||||
keyValue = val.String()
|
||||
default:
|
||||
return fmt.Errorf("Only map[string]string are supported (XXX)")
|
||||
}
|
||||
params.Set(fmt.Sprintf("%s[%d].%s", prefix, i, keyName), keyValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractJSONTag returns the variable name or defaultName as well as if the field is required (!omitempty)
|
||||
func extractJSONTag(defaultName, jsonTag string) (string, bool) {
|
||||
tags := strings.Split(jsonTag, ",")
|
||||
name := tags[0]
|
||||
required := true
|
||||
for _, tag := range tags {
|
||||
if tag == "omitempty" {
|
||||
required = false
|
||||
}
|
||||
}
|
||||
|
||||
if name == "" || name == "omitempty" {
|
||||
name = defaultName
|
||||
}
|
||||
return name, required
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue