435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
303 lines
8 KiB
Go
303 lines
8 KiB
Go
// Copyright (C) 2015 . All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE.md file.
|
|
|
|
// Interact with API
|
|
|
|
// Package api contains client and functions to interact with API
|
|
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// https://cp-par1.scaleway.com/products/servers
|
|
// https://cp-ams1.scaleway.com/products/servers
|
|
// Default values
|
|
var (
|
|
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/"
|
|
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
|
|
password string // Password is the authentication password
|
|
userAgent string
|
|
computeAPI string
|
|
|
|
Region string
|
|
}
|
|
|
|
// APIError represents a API Error
|
|
type APIError struct {
|
|
// 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
|
|
func (e APIError) Error() string {
|
|
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()
|
|
}
|
|
|
|
// New creates a ready-to-use SDK client
|
|
func New(organization, token, region string, options ...func(*API)) (*API, error) {
|
|
s := &API{
|
|
// exposed
|
|
Organization: organization,
|
|
Token: token,
|
|
Client: &http.Client{},
|
|
|
|
// internal
|
|
password: "",
|
|
userAgent: "scaleway-sdk",
|
|
}
|
|
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
|
|
}
|
|
|
|
func (s *API) response(method, uri string, content io.Reader) (resp *http.Response, err error) {
|
|
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)
|
|
resp, err = s.Client.Do(req)
|
|
return
|
|
}
|
|
|
|
// GetResponsePaginate fetchs all resources and returns an http.Response object for the requested resource
|
|
func (s *API) GetResponsePaginate(apiURL, resource string, values url.Values) (*http.Response, error) {
|
|
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
|
|
func (s *API) PostResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
|
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
|
|
func (s *API) PatchResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
|
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
|
|
func (s *API) PutResponse(apiURL, resource string, data interface{}) (*http.Response, error) {
|
|
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
|
|
func (s *API) DeleteResponse(apiURL, resource string) (*http.Response, error) {
|
|
return s.response("DELETE", fmt.Sprintf("%s/%s", strings.TrimRight(apiURL, "/"), resource), nil)
|
|
}
|
|
|
|
// handleHTTPError checks the statusCode and displays the error
|
|
func (s *API) handleHTTPError(goodStatusCode []int, resp *http.Response) ([]byte, error) {
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode >= http.StatusInternalServerError {
|
|
return nil, errors.New(string(body))
|
|
}
|
|
|
|
for _, code := range goodStatusCode {
|
|
if code == resp.StatusCode {
|
|
return body, nil
|
|
}
|
|
}
|
|
|
|
var scwError APIError
|
|
scwError.StatusCode = resp.StatusCode
|
|
if len(body) > 0 {
|
|
if err := json.Unmarshal(body, &scwError); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, scwError
|
|
}
|
|
|
|
// SetPassword register the password
|
|
func (s *API) SetPassword(password string) {
|
|
s.password = password
|
|
}
|