packer-cn/vendor/github.com/scaleway/scaleway-sdk-go/scw/client.go

377 lines
10 KiB
Go

package scw
import (
"crypto/tls"
"encoding/json"
"io"
"math"
"net"
"net/http"
"net/http/httputil"
"reflect"
"strconv"
"sync/atomic"
"time"
"github.com/scaleway/scaleway-sdk-go/internal/auth"
"github.com/scaleway/scaleway-sdk-go/internal/errors"
"github.com/scaleway/scaleway-sdk-go/logger"
)
// Client is the Scaleway client which performs API requests.
//
// This client should be passed in the `NewApi` functions whenever an API instance is created.
// Creating a Client is done with the `NewClient` function.
type Client struct {
httpClient httpClient
auth auth.Auth
apiURL string
userAgent string
defaultOrganizationID *string
defaultProjectID *string
defaultRegion *Region
defaultZone *Zone
defaultPageSize *uint32
}
func defaultOptions() []ClientOption {
return []ClientOption{
WithoutAuth(),
WithAPIURL("https://api.scaleway.com"),
withDefaultUserAgent(userAgent),
}
}
// NewClient instantiate a new Client object.
//
// Zero or more ClientOption object can be passed as a parameter.
// These options will then be applied to the client.
func NewClient(opts ...ClientOption) (*Client, error) {
s := newSettings()
// apply options
s.apply(append(defaultOptions(), opts...))
// validate settings
err := s.validate()
if err != nil {
return nil, err
}
// dial the API
if s.httpClient == nil {
s.httpClient = newHTTPClient()
}
// insecure mode
if s.insecure {
logger.Debugf("client: using insecure mode")
setInsecureMode(s.httpClient)
}
logger.Debugf("client: using sdk version " + version)
return &Client{
auth: s.token,
httpClient: s.httpClient,
apiURL: s.apiURL,
userAgent: s.userAgent,
defaultOrganizationID: s.defaultOrganizationID,
defaultProjectID: s.defaultProjectID,
defaultRegion: s.defaultRegion,
defaultZone: s.defaultZone,
defaultPageSize: s.defaultPageSize,
}, nil
}
// GetDefaultOrganizationID returns the default organization ID
// of the client. This value can be set in the client option
// WithDefaultOrganizationID(). Be aware this value can be empty.
func (c *Client) GetDefaultOrganizationID() (organizationID string, exists bool) {
if c.defaultOrganizationID != nil {
return *c.defaultOrganizationID, true
}
return "", false
}
// GetDefaultProjectID returns the default project ID
// of the client. This value can be set in the client option
// WithDefaultProjectID(). Be aware this value can be empty.
func (c *Client) GetDefaultProjectID() (projectID string, exists bool) {
if c.defaultProjectID != nil {
return *c.defaultProjectID, true
}
return "", false
}
// GetDefaultRegion returns the default region of the client.
// This value can be set in the client option
// WithDefaultRegion(). Be aware this value can be empty.
func (c *Client) GetDefaultRegion() (region Region, exists bool) {
if c.defaultRegion != nil {
return *c.defaultRegion, true
}
return Region(""), false
}
// GetDefaultZone returns the default zone of the client.
// This value can be set in the client option
// WithDefaultZone(). Be aware this value can be empty.
func (c *Client) GetDefaultZone() (zone Zone, exists bool) {
if c.defaultZone != nil {
return *c.defaultZone, true
}
return Zone(""), false
}
func (c *Client) GetSecretKey() (secretKey string, exists bool) {
if token, isToken := c.auth.(*auth.Token); isToken {
return token.SecretKey, isToken
}
return "", false
}
func (c *Client) GetAccessKey() (accessKey string, exists bool) {
if token, isToken := c.auth.(*auth.Token); isToken {
return token.AccessKey, isToken
}
return "", false
}
// GetDefaultPageSize returns the default page size of the client.
// This value can be set in the client option
// WithDefaultPageSize(). Be aware this value can be empty.
func (c *Client) GetDefaultPageSize() (pageSize uint32, exists bool) {
if c.defaultPageSize != nil {
return *c.defaultPageSize, true
}
return 0, false
}
// Do performs HTTP request(s) based on the ScalewayRequest object.
// RequestOptions are applied prior to doing the request.
func (c *Client) Do(req *ScalewayRequest, res interface{}, opts ...RequestOption) (err error) {
// apply request options
req.apply(opts)
// validate request options
err = req.validate()
if err != nil {
return err
}
if req.auth == nil {
req.auth = c.auth
}
if req.allPages {
return c.doListAll(req, res)
}
return c.do(req, res)
}
// requestNumber auto increments on each do().
// This allows easy distinguishing of concurrently performed requests in log.
var requestNumber uint32
// do performs a single HTTP request based on the ScalewayRequest object.
func (c *Client) do(req *ScalewayRequest, res interface{}) (sdkErr error) {
currentRequestNumber := atomic.AddUint32(&requestNumber, 1)
if req == nil {
return errors.New("request must be non-nil")
}
// build url
url, sdkErr := req.getURL(c.apiURL)
if sdkErr != nil {
return sdkErr
}
logger.Debugf("creating %s request on %s", req.Method, url.String())
// build request
httpRequest, err := http.NewRequest(req.Method, url.String(), req.Body)
if err != nil {
return errors.Wrap(err, "could not create request")
}
httpRequest.Header = req.getAllHeaders(req.auth, c.userAgent, false)
if req.ctx != nil {
httpRequest = httpRequest.WithContext(req.ctx)
}
if logger.ShouldLog(logger.LogLevelDebug) {
// Keep original headers (before anonymization)
originalHeaders := httpRequest.Header
// Get anonymized headers
httpRequest.Header = req.getAllHeaders(req.auth, c.userAgent, true)
dump, err := httputil.DumpRequestOut(httpRequest, true)
if err != nil {
logger.Warningf("cannot dump outgoing request: %s", err)
} else {
var logString string
logString += "\n--------------- Scaleway SDK REQUEST %d : ---------------\n"
logString += "%s\n"
logString += "---------------------------------------------------------"
logger.Debugf(logString, currentRequestNumber, dump)
}
// Restore original headers before sending the request
httpRequest.Header = originalHeaders
}
// execute request
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return errors.Wrap(err, "error executing request")
}
defer func() {
closeErr := httpResponse.Body.Close()
if sdkErr == nil && closeErr != nil {
sdkErr = errors.Wrap(closeErr, "could not close http response")
}
}()
if logger.ShouldLog(logger.LogLevelDebug) {
dump, err := httputil.DumpResponse(httpResponse, true)
if err != nil {
logger.Warningf("cannot dump ingoing response: %s", err)
} else {
var logString string
logString += "\n--------------- Scaleway SDK RESPONSE %d : ---------------\n"
logString += "%s\n"
logString += "----------------------------------------------------------"
logger.Debugf(logString, currentRequestNumber, dump)
}
}
sdkErr = hasResponseError(httpResponse)
if sdkErr != nil {
return sdkErr
}
if res != nil {
contentType := httpResponse.Header.Get("Content-Type")
switch contentType {
case "application/json":
err = json.NewDecoder(httpResponse.Body).Decode(&res)
if err != nil {
return errors.Wrap(err, "could not parse %s response body", contentType)
}
default:
buffer, isBuffer := res.(io.Writer)
if !isBuffer {
return errors.Wrap(err, "could not handle %s response body with %T result type", contentType, buffer)
}
_, err := io.Copy(buffer, httpResponse.Body)
if err != nil {
return errors.Wrap(err, "could not copy %s response body", contentType)
}
}
// Handle instance API X-Total-Count header
xTotalCountStr := httpResponse.Header.Get("X-Total-Count")
if legacyLister, isLegacyLister := res.(legacyLister); isLegacyLister && xTotalCountStr != "" {
xTotalCount, err := strconv.Atoi(xTotalCountStr)
if err != nil {
return errors.Wrap(err, "could not parse X-Total-Count header")
}
legacyLister.UnsafeSetTotalCount(xTotalCount)
}
}
return nil
}
type lister interface {
UnsafeGetTotalCount() uint32
UnsafeAppend(interface{}) (uint32, error)
}
type legacyLister interface {
UnsafeSetTotalCount(totalCount int)
}
const maxPageCount uint32 = math.MaxUint32
// doListAll collects all pages of a List request and aggregate all results on a single response.
func (c *Client) doListAll(req *ScalewayRequest, res interface{}) (err error) {
// check for lister interface
if response, isLister := res.(lister); isLister {
pageCount := maxPageCount
for page := uint32(1); page <= pageCount; page++ {
// set current page
req.Query.Set("page", strconv.FormatUint(uint64(page), 10))
// request the next page
nextPage := newVariableFromType(response)
err := c.do(req, nextPage)
if err != nil {
return err
}
// append results
pageSize, err := response.UnsafeAppend(nextPage)
if err != nil {
return err
}
if pageSize == 0 {
return nil
}
// set total count on first request
if pageCount == maxPageCount {
totalCount := nextPage.(lister).UnsafeGetTotalCount()
pageCount = (totalCount + pageSize - 1) / pageSize
}
}
return nil
}
return errors.New("%T does not support pagination", res)
}
// newVariableFromType returns a variable set to the zero value of the given type
func newVariableFromType(t interface{}) interface{} {
// reflect.New always create a pointer, that's why we use reflect.Indirect before
return reflect.New(reflect.Indirect(reflect.ValueOf(t)).Type()).Interface()
}
func newHTTPClient() *http.Client {
return &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
MaxIdleConnsPerHost: 20,
},
}
}
func setInsecureMode(c httpClient) {
standardHTTPClient, ok := c.(*http.Client)
if !ok {
logger.Warningf("client: cannot use insecure mode with HTTP client of type %T", c)
return
}
transportClient, ok := standardHTTPClient.Transport.(*http.Transport)
if !ok {
logger.Warningf("client: cannot use insecure mode with Transport client of type %T", standardHTTPClient.Transport)
return
}
if transportClient.TLSClientConfig == nil {
transportClient.TLSClientConfig = &tls.Config{}
}
transportClient.TLSClientConfig.InsecureSkipVerify = true
}