gogreen/godo.go

463 lines
12 KiB
Go

package godo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/google/go-querystring/query"
"golang.org/x/oauth2"
)
const (
libraryVersion = "1.81.0"
defaultBaseURL = "https://service.greenhost.net/"
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
headerRateLimit = "RateLimit-Limit"
headerRateRemaining = "RateLimit-Remaining"
headerRateReset = "RateLimit-Reset"
)
// Client manages communication with DigitalOcean V2 API.
type Client struct {
// HTTP client used to communicate with the DO API.
client *http.Client
// Base URL for API requests.
BaseURL *url.URL
// User agent for client
UserAgent string
// Rate contains the current rate limit for the client as determined by the most recent
// API call. It is not thread-safe. Please consider using GetRate() instead.
Rate Rate
ratemtx sync.Mutex
// Services used for communicating with the API
Actions ActionsService
Droplets DropletsService
DropletActions DropletActionsService
Images ImagesService
ImageActions ImageActionsService
Keys KeysService
Regions RegionsService
// Optional function called after every successful request made to the DO APIs
onRequestCompleted RequestCompletionCallback
// Optional extra HTTP headers to set on every request to the API.
headers map[string]string
}
// RequestCompletionCallback defines the type of the request callback function
type RequestCompletionCallback func(*http.Request, *http.Response)
// ListOptions specifies the optional parameters to various List methods that
// support pagination.
type ListOptions struct {
// For paginated result sets, page of results to retrieve.
Page int `url:"page,omitempty"`
// For paginated result sets, the number of results to include per page.
PerPage int `url:"per_page,omitempty"`
}
// TokenListOptions specifies the optional parameters to various List methods that support token pagination.
type TokenListOptions struct {
// For paginated result sets, page of results to retrieve.
Page int `url:"page,omitempty"`
// For paginated result sets, the number of results to include per page.
PerPage int `url:"per_page,omitempty"`
// For paginated result sets which support tokens, the token provided by the last set
// of results in order to retrieve the next set of results. This is expected to be faster
// than incrementing or decrementing the page number.
Token string `url:"page_token,omitempty"`
}
// Response is a DigitalOcean response. This wraps the standard http.Response returned from DigitalOcean.
type Response struct {
*http.Response
// Links that were returned with the response. These are parsed from
// request body and not the header.
Links []*LinkAction
// Meta describes generic information about the response.
Meta *Meta
// Monitoring URI
// Deprecated: This field is not populated. To poll for the status of a
// newly created Droplet, use Links.Actions[0].HREF
Monitor string
Rate
}
// An ErrorResponse reports the error caused by an API request
type ErrorResponse struct {
// HTTP response that caused this error
Response *http.Response
// Error message
Message string `json:"message"`
// RequestID returned from the API, useful to contact support.
RequestID string `json:"request_id"`
}
// Rate contains the rate limit for the current client.
type Rate struct {
// The number of request per hour the client is currently limited to.
Limit int `json:"limit"`
// The number of remaining requests the client can make this hour.
Remaining int `json:"remaining"`
// The time at which the current rate limit will reset.
Reset Timestamp `json:"reset"`
}
func addOptions(s string, opt interface{}) (string, error) {
v := reflect.ValueOf(opt)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
origURL, err := url.Parse(s)
if err != nil {
return s, err
}
origValues := origURL.Query()
newValues, err := query.Values(opt)
if err != nil {
return s, err
}
for k, v := range newValues {
origValues[k] = v
}
origURL.RawQuery = origValues.Encode()
return origURL.String(), nil
}
// NewFromToken returns a new DigitalOcean API client with the given API
// token.
func NewFromToken(token string) *Client {
cleanToken := strings.Trim(strings.TrimSpace(token), "'")
ctx := context.Background()
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: cleanToken})
return NewClient(oauth2.NewClient(ctx, ts))
}
// NewClient returns a new DigitalOcean API client, using the given
// http.Client to perform all requests.
//
// Users who wish to pass their own http.Client should use this method. If
// you're in need of further customization, the godo.New method allows more
// options, such as setting a custom URL or a custom user agent string.
func NewClient(httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
baseURL, _ := url.Parse(defaultBaseURL)
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
c.Actions = &ActionsServiceOp{client: c}
c.Droplets = &DropletsServiceOp{client: c}
c.DropletActions = &DropletActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Keys = &KeysServiceOp{client: c}
c.Regions = &RegionsServiceOp{client: c}
c.headers = make(map[string]string)
return c
}
// ClientOpt are options for New.
type ClientOpt func(*Client) error
// New returns a new DigitalOcean API client instance.
func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) {
c := NewClient(httpClient)
for _, opt := range opts {
if err := opt(c); err != nil {
return nil, err
}
}
return c, nil
}
// SetBaseURL is a client option for setting the base URL.
func SetBaseURL(bu string) ClientOpt {
return func(c *Client) error {
u, err := url.Parse(bu)
if err != nil {
return err
}
c.BaseURL = u
return nil
}
}
// SetUserAgent is a client option for setting the user agent.
func SetUserAgent(ua string) ClientOpt {
return func(c *Client) error {
c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent)
return nil
}
}
// SetRequestHeaders sets optional HTTP headers on the client that are
// sent on each HTTP request.
func SetRequestHeaders(headers map[string]string) ClientOpt {
return func(c *Client) error {
for k, v := range headers {
c.headers[k] = v
}
return nil
}
}
// NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the
// value pointed to by body is JSON encoded and included in as the request body.
func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
u, err := c.BaseURL.Parse(urlStr)
if err != nil {
return nil, err
}
var req *http.Request
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
req, err = http.NewRequest(method, u.String(), nil)
if err != nil {
return nil, err
}
default:
buf := new(bytes.Buffer)
if body != nil {
err = json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, err
}
}
req, err = http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", mediaType)
}
for k, v := range c.headers {
req.Header.Add(k, v)
}
req.Header.Set("Accept", mediaType)
req.Header.Set("User-Agent", c.UserAgent)
return req, nil
}
// OnRequestCompleted sets the DO API request completion callback
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
c.onRequestCompleted = rc
}
// GetRate returns the current rate limit for the client as determined by the most recent
// API call. It is thread-safe.
func (c *Client) GetRate() Rate {
c.ratemtx.Lock()
defer c.ratemtx.Unlock()
return c.Rate
}
// newResponse creates a new Response for the provided http.Response
func newResponse(r *http.Response) *Response {
response := Response{Response: r}
response.populateRate()
return &response
}
// populateRate parses the rate related headers and populates the response Rate.
func (r *Response) populateRate() {
if limit := r.Header.Get(headerRateLimit); limit != "" {
r.Rate.Limit, _ = strconv.Atoi(limit)
}
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
r.Rate.Remaining, _ = strconv.Atoi(remaining)
}
if reset := r.Header.Get(headerRateReset); reset != "" {
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
r.Rate.Reset = Timestamp{time.Unix(v, 0)}
}
}
}
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
// the raw response will be written to v, without attempting to decode it.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
resp, err := DoRequestWithClient(ctx, c.client, req)
if err != nil {
return nil, err
}
if c.onRequestCompleted != nil {
c.onRequestCompleted(req, resp)
}
defer func() {
// Ensure the response body is fully read and closed
// before we reconnect, so that we reuse the same TCPConnection.
// Close the previous response's body. But read at least some of
// the body so if it's small the underlying TCP connection will be
// re-used. No need to check for errors: if it fails, the Transport
// won't reuse it anyway.
const maxBodySlurpSize = 2 << 10
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
}
if rerr := resp.Body.Close(); err == nil {
err = rerr
}
}()
response := newResponse(resp)
c.ratemtx.Lock()
c.Rate = response.Rate
c.ratemtx.Unlock()
err = CheckResponse(resp)
if err != nil {
return response, err
}
if v != nil {
if w, ok := v.(io.Writer); ok {
_, err = io.Copy(w, resp.Body)
if err != nil {
return nil, err
}
} else {
err = json.NewDecoder(resp.Body).Decode(v)
if err != nil {
return nil, err
}
}
}
return response, err
}
// DoRequest submits an HTTP request.
func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
return DoRequestWithClient(ctx, http.DefaultClient, req)
}
// DoRequestWithClient submits an HTTP request using the specified client.
func DoRequestWithClient(
ctx context.Context,
client *http.Client,
req *http.Request) (*http.Response, error) {
req = req.WithContext(ctx)
return client.Do(req)
}
func (r *ErrorResponse) Error() string {
if r.RequestID != "" {
return fmt.Sprintf("%v %v: %d (request %q) %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
}
return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
}
// CheckResponse checks the API response for errors, and returns them if present. A response is considered an
// error if it has a status code outside the 200 range. API error responses are expected to have either no response
// body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored.
// If the API error response does not include the request ID in its body, the one from its header will be used.
func CheckResponse(r *http.Response) error {
if c := r.StatusCode; c >= 200 && c <= 299 {
return nil
}
errorResponse := &ErrorResponse{Response: r}
data, err := ioutil.ReadAll(r.Body)
if err == nil && len(data) > 0 {
err := json.Unmarshal(data, errorResponse)
if err != nil {
errorResponse.Message = string(data)
}
}
if errorResponse.RequestID == "" {
errorResponse.RequestID = r.Header.Get("x-request-id")
}
return errorResponse
}
func (r Rate) String() string {
return Stringify(r)
}
// String is a helper routine that allocates a new string value
// to store v and returns a pointer to it.
func String(v string) *string {
p := new(string)
*p = v
return p
}
// Int is a helper routine that allocates a new int32 value
// to store v and returns a pointer to it, but unlike Int32
// its argument value is an int.
func Int(v int) *int {
p := new(int)
*p = v
return p
}
// Bool is a helper routine that allocates a new bool value
// to store v and returns a pointer to it.
func Bool(v bool) *bool {
p := new(bool)
*p = v
return p
}
// StreamToString converts a reader to a string
func StreamToString(stream io.Reader) string {
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(stream)
return buf.String()
}