2018-09-05 20:31:10 +00:00
|
|
|
// Copyright (C) 2015 . All rights reserved.
|
2017-10-31 22:03:54 +00:00
|
|
|
// Use of this source code is governed by a MIT-style
|
|
|
|
// license that can be found in the LICENSE.md file.
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
// Interact with API
|
2017-10-31 22:03:54 +00:00
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
// Package api contains client and functions to interact with API
|
2017-10-31 22:03:54 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
)
|
|
|
|
|
2019-03-26 21:50:42 +00:00
|
|
|
// https://cp-par1.scaleway.com/products/servers
|
|
|
|
// https://cp-ams1.scaleway.com/products/servers
|
2017-10-31 22:03:54 +00:00
|
|
|
// Default values
|
|
|
|
var (
|
2018-09-05 20:31:10 +00:00
|
|
|
AccountAPI = "https://account.scaleway.com/"
|
|
|
|
MetadataAPI = "http://169.254.42.42/"
|
|
|
|
MarketplaceAPI = "https://api-marketplace.scaleway.com"
|
|
|
|
ComputeAPIPar1 = "https://cp-par1.scaleway.com/"
|
|
|
|
ComputeAPIAms1 = "https://cp-ams1.scaleway.com/"
|
2017-10-31 22:03:54 +00:00
|
|
|
|
|
|
|
URLPublicDNS = ".pub.cloud.scaleway.com"
|
|
|
|
URLPrivateDNS = ".priv.cloud.scaleway.com"
|
|
|
|
)
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
if url := os.Getenv("SCW_ACCOUNT_API"); url != "" {
|
|
|
|
AccountAPI = url
|
|
|
|
}
|
|
|
|
if url := os.Getenv("SCW_METADATA_API"); url != "" {
|
|
|
|
MetadataAPI = url
|
|
|
|
}
|
|
|
|
if url := os.Getenv("SCW_MARKETPLACE_API"); url != "" {
|
|
|
|
MarketplaceAPI = url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
perPage = 50
|
|
|
|
)
|
|
|
|
|
|
|
|
// HTTPClient wraps the net/http Client Do method
|
|
|
|
type HTTPClient interface {
|
|
|
|
Do(*http.Request) (*http.Response, error)
|
|
|
|
}
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
// API is the interface used to communicate with the API
|
|
|
|
type API struct {
|
|
|
|
Organization string // Organization is the identifier of the organization
|
|
|
|
Token string // Token is the authentication token for the organization
|
|
|
|
Client HTTPClient // Client is used for all HTTP interactions
|
2017-10-31 22:03:54 +00:00
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
password string // Password is the authentication password
|
|
|
|
userAgent string
|
|
|
|
computeAPI string
|
2017-10-31 22:03:54 +00:00
|
|
|
|
|
|
|
Region string
|
|
|
|
}
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
// APIError represents a API Error
|
|
|
|
type APIError struct {
|
2017-10-31 22:03:54 +00:00
|
|
|
// Message is a human-friendly error message
|
|
|
|
APIMessage string `json:"message,omitempty"`
|
|
|
|
|
|
|
|
// Type is a string code that defines the kind of error
|
|
|
|
Type string `json:"type,omitempty"`
|
|
|
|
|
|
|
|
// Fields contains detail about validation error
|
|
|
|
Fields map[string][]string `json:"fields,omitempty"`
|
|
|
|
|
|
|
|
// StatusCode is the HTTP status code received
|
|
|
|
StatusCode int `json:"-"`
|
|
|
|
|
|
|
|
// Message
|
|
|
|
Message string `json:"-"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error returns a string representing the error
|
2018-09-05 20:31:10 +00:00
|
|
|
func (e APIError) Error() string {
|
2017-10-31 22:03:54 +00:00
|
|
|
var b bytes.Buffer
|
|
|
|
|
|
|
|
fmt.Fprintf(&b, "StatusCode: %v, ", e.StatusCode)
|
|
|
|
fmt.Fprintf(&b, "Type: %v, ", e.Type)
|
|
|
|
fmt.Fprintf(&b, "APIMessage: \x1b[31m%v\x1b[0m", e.APIMessage)
|
|
|
|
if len(e.Fields) > 0 {
|
|
|
|
fmt.Fprintf(&b, ", Details: %v", e.Fields)
|
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
// New creates a ready-to-use SDK client
|
|
|
|
func New(organization, token, region string, options ...func(*API)) (*API, error) {
|
|
|
|
s := &API{
|
2017-10-31 22:03:54 +00:00
|
|
|
// exposed
|
|
|
|
Organization: organization,
|
|
|
|
Token: token,
|
2018-09-05 20:31:10 +00:00
|
|
|
Client: &http.Client{},
|
2017-10-31 22:03:54 +00:00
|
|
|
|
|
|
|
// internal
|
|
|
|
password: "",
|
2019-03-26 21:50:42 +00:00
|
|
|
userAgent: "scaleway-sdk",
|
2017-10-31 22:03:54 +00:00
|
|
|
}
|
|
|
|
for _, option := range options {
|
|
|
|
option(s)
|
|
|
|
}
|
|
|
|
switch region {
|
|
|
|
case "par1", "":
|
|
|
|
s.computeAPI = ComputeAPIPar1
|
|
|
|
case "ams1":
|
|
|
|
s.computeAPI = ComputeAPIAms1
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("%s isn't a valid region", region)
|
|
|
|
}
|
|
|
|
s.Region = region
|
|
|
|
if url := os.Getenv("SCW_COMPUTE_API"); url != "" {
|
|
|
|
s.computeAPI = url
|
|
|
|
}
|
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) response(method, uri string, content io.Reader) (resp *http.Response, err error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
var (
|
|
|
|
req *http.Request
|
|
|
|
)
|
|
|
|
|
|
|
|
req, err = http.NewRequest(method, uri, content)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("response %s %s", method, uri)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
req.Header.Set("X-Auth-Token", s.Token)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
req.Header.Set("User-Agent", s.userAgent)
|
2018-09-05 20:31:10 +00:00
|
|
|
resp, err = s.Client.Do(req)
|
2017-10-31 22:03:54 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetResponsePaginate fetchs all resources and returns an http.Response object for the requested resource
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) GetResponsePaginate(apiURL, resource string, values url.Values) (*http.Response, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
resp, err := s.response("HEAD", fmt.Sprintf("%s/%s?%s", strings.TrimRight(apiURL, "/"), resource, values.Encode()), nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
count := resp.Header.Get("X-Total-Count")
|
|
|
|
var maxElem int
|
|
|
|
if count == "" {
|
|
|
|
maxElem = 0
|
|
|
|
} else {
|
|
|
|
maxElem, err = strconv.Atoi(count)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get := maxElem / perPage
|
|
|
|
if (float32(maxElem) / perPage) > float32(get) {
|
|
|
|
get++
|
|
|
|
}
|
|
|
|
|
|
|
|
if get <= 1 { // If there is 0 or 1 page of result, the response is not paginated
|
|
|
|
if len(values) == 0 {
|
|
|
|
return s.response("GET", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), nil)
|
|
|
|
}
|
|
|
|
return s.response("GET", fmt.Sprintf("%s/%s?%s", strings.TrimRight(apiURL, "/"), resource, values.Encode()), nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchAll := !(values.Get("per_page") != "" || values.Get("page") != "")
|
|
|
|
if fetchAll {
|
|
|
|
var g errgroup.Group
|
|
|
|
|
|
|
|
ch := make(chan *http.Response, get)
|
|
|
|
for i := 1; i <= get; i++ {
|
|
|
|
i := i // closure tricks
|
|
|
|
g.Go(func() (err error) {
|
|
|
|
var resp *http.Response
|
|
|
|
|
|
|
|
val := url.Values{}
|
|
|
|
val.Set("per_page", fmt.Sprintf("%v", perPage))
|
|
|
|
val.Set("page", fmt.Sprintf("%v", i))
|
|
|
|
resp, err = s.response("GET", fmt.Sprintf("%s/%s?%s", strings.TrimRight(apiURL, "/"), resource, val.Encode()), nil)
|
|
|
|
ch <- resp
|
|
|
|
return
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if err = g.Wait(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newBody := make(map[string][]json.RawMessage)
|
|
|
|
body := make(map[string][]json.RawMessage)
|
|
|
|
key := ""
|
|
|
|
for i := 0; i < get; i++ {
|
|
|
|
res := <-ch
|
|
|
|
if res.StatusCode != http.StatusOK {
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
content, err := ioutil.ReadAll(res.Body)
|
|
|
|
res.Body.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(content, &body); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == 0 {
|
|
|
|
resp = res
|
|
|
|
for k := range body {
|
|
|
|
key = k
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
newBody[key] = append(newBody[key], body[key]...)
|
|
|
|
}
|
|
|
|
payload := new(bytes.Buffer)
|
|
|
|
if err := json.NewEncoder(payload).Encode(newBody); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
resp.Body = ioutil.NopCloser(payload)
|
|
|
|
} else {
|
|
|
|
resp, err = s.response("GET", fmt.Sprintf("%s/%s?%s", strings.TrimRight(apiURL, "/"), resource, values.Encode()), nil)
|
|
|
|
}
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// PostResponse returns an http.Response object for the updated resource
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) PostResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
payload := new(bytes.Buffer)
|
|
|
|
if err := json.NewEncoder(payload).Encode(data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return s.response("POST", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PatchResponse returns an http.Response object for the updated resource
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) PatchResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
payload := new(bytes.Buffer)
|
|
|
|
if err := json.NewEncoder(payload).Encode(data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return s.response("PATCH", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PutResponse returns an http.Response object for the updated resource
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) PutResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
payload := new(bytes.Buffer)
|
|
|
|
if err := json.NewEncoder(payload).Encode(data); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return s.response("PUT", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), payload)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeleteResponse returns an http.Response object for the deleted resource
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) DeleteResponse(apiURL, resource string) (*http.Response, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
return s.response("DELETE", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleHTTPError checks the statusCode and displays the error
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) handleHTTPError(goodStatusCode []int, resp *http.Response) ([]byte, error) {
|
2017-10-31 22:03:54 +00:00
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode >= http.StatusInternalServerError {
|
|
|
|
return nil, errors.New(string(body))
|
|
|
|
}
|
2018-09-05 20:31:10 +00:00
|
|
|
|
2017-10-31 22:03:54 +00:00
|
|
|
for _, code := range goodStatusCode {
|
|
|
|
if code == resp.StatusCode {
|
2018-09-05 20:31:10 +00:00
|
|
|
return body, nil
|
2017-10-31 22:03:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-05 20:31:10 +00:00
|
|
|
var scwError APIError
|
|
|
|
scwError.StatusCode = resp.StatusCode
|
|
|
|
if len(body) > 0 {
|
2017-10-31 22:03:54 +00:00
|
|
|
if err := json.Unmarshal(body, &scwError); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2018-09-05 20:31:10 +00:00
|
|
|
return nil, scwError
|
2017-10-31 22:03:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetPassword register the password
|
2018-09-05 20:31:10 +00:00
|
|
|
func (s *API) SetPassword(password string) {
|
2017-10-31 22:03:54 +00:00
|
|
|
s.password = password
|
|
|
|
}
|