2020-06-18 08:04:41 -07:00
|
|
|
package godo
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"reflect"
|
|
|
|
"strconv"
|
2020-07-27 02:08:38 -07:00
|
|
|
"sync"
|
2020-06-18 08:04:41 -07:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/google/go-querystring/query"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-24 05:03:47 -07:00
|
|
|
libraryVersion = "1.42.1"
|
2020-06-18 08:04:41 -07:00
|
|
|
defaultBaseURL = "https://api.digitalocean.com/"
|
|
|
|
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
|
2020-07-27 02:08:38 -07:00
|
|
|
// API call. It is not thread-safe. Please consider using GetRate() instead.
|
|
|
|
Rate Rate
|
|
|
|
ratemtx sync.Mutex
|
2020-06-18 08:04:41 -07:00
|
|
|
|
|
|
|
// Services used for communicating with the API
|
|
|
|
Account AccountService
|
|
|
|
Actions ActionsService
|
2020-07-27 02:08:38 -07:00
|
|
|
Apps AppsService
|
2020-06-18 08:04:41 -07:00
|
|
|
Balance BalanceService
|
|
|
|
BillingHistory BillingHistoryService
|
|
|
|
CDNs CDNService
|
|
|
|
Domains DomainsService
|
|
|
|
Droplets DropletsService
|
|
|
|
DropletActions DropletActionsService
|
|
|
|
Images ImagesService
|
|
|
|
ImageActions ImageActionsService
|
|
|
|
Invoices InvoicesService
|
|
|
|
Keys KeysService
|
|
|
|
Regions RegionsService
|
|
|
|
Sizes SizesService
|
|
|
|
FloatingIPs FloatingIPsService
|
|
|
|
FloatingIPActions FloatingIPActionsService
|
|
|
|
Snapshots SnapshotsService
|
|
|
|
Storage StorageService
|
|
|
|
StorageActions StorageActionsService
|
|
|
|
Tags TagsService
|
|
|
|
LoadBalancers LoadBalancersService
|
|
|
|
Certificates CertificatesService
|
|
|
|
Firewalls FirewallsService
|
|
|
|
Projects ProjectsService
|
|
|
|
Kubernetes KubernetesService
|
|
|
|
Registry RegistryService
|
|
|
|
Databases DatabasesService
|
|
|
|
VPCs VPCsService
|
|
|
|
OneClick OneClickService
|
|
|
|
|
|
|
|
// Optional function called after every successful request made to the DO APIs
|
|
|
|
onRequestCompleted RequestCompletionCallback
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 *Links
|
|
|
|
|
|
|
|
// Meta describes generic information about the response.
|
|
|
|
Meta *Meta
|
|
|
|
|
|
|
|
// Monitoring URI
|
|
|
|
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 {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
config := &oauth2.Config{}
|
|
|
|
ts := config.TokenSource(ctx, &oauth2.Token{AccessToken: token})
|
|
|
|
|
|
|
|
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.Account = &AccountServiceOp{client: c}
|
|
|
|
c.Actions = &ActionsServiceOp{client: c}
|
2020-07-27 02:08:38 -07:00
|
|
|
c.Apps = &AppsServiceOp{client: c}
|
2020-06-18 08:04:41 -07:00
|
|
|
c.Balance = &BalanceServiceOp{client: c}
|
|
|
|
c.BillingHistory = &BillingHistoryServiceOp{client: c}
|
|
|
|
c.CDNs = &CDNServiceOp{client: c}
|
|
|
|
c.Certificates = &CertificatesServiceOp{client: c}
|
|
|
|
c.Domains = &DomainsServiceOp{client: c}
|
|
|
|
c.Droplets = &DropletsServiceOp{client: c}
|
|
|
|
c.DropletActions = &DropletActionsServiceOp{client: c}
|
|
|
|
c.Firewalls = &FirewallsServiceOp{client: c}
|
|
|
|
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
|
|
|
|
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
|
|
|
|
c.Images = &ImagesServiceOp{client: c}
|
|
|
|
c.ImageActions = &ImageActionsServiceOp{client: c}
|
|
|
|
c.Invoices = &InvoicesServiceOp{client: c}
|
|
|
|
c.Keys = &KeysServiceOp{client: c}
|
|
|
|
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
|
|
|
|
c.Projects = &ProjectsServiceOp{client: c}
|
|
|
|
c.Regions = &RegionsServiceOp{client: c}
|
|
|
|
c.Sizes = &SizesServiceOp{client: c}
|
|
|
|
c.Snapshots = &SnapshotsServiceOp{client: c}
|
|
|
|
c.Storage = &StorageServiceOp{client: c}
|
|
|
|
c.StorageActions = &StorageActionsServiceOp{client: c}
|
|
|
|
c.Tags = &TagsServiceOp{client: c}
|
|
|
|
c.Kubernetes = &KubernetesServiceOp{client: c}
|
|
|
|
c.Registry = &RegistryServiceOp{client: c}
|
|
|
|
c.Databases = &DatabasesServiceOp{client: c}
|
|
|
|
c.VPCs = &VPCsServiceOp{client: c}
|
|
|
|
c.OneClick = &OneClickServiceOp{client: c}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
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.Add("Content-Type", mediaType)
|
|
|
|
req.Header.Add("Accept", mediaType)
|
|
|
|
req.Header.Add("User-Agent", c.UserAgent)
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnRequestCompleted sets the DO API request completion callback
|
|
|
|
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
|
|
|
|
c.onRequestCompleted = rc
|
|
|
|
}
|
|
|
|
|
2020-07-27 02:08:38 -07:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-06-18 08:04:41 -07:00
|
|
|
// 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() {
|
2020-07-27 02:08:38 -07:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2020-06-18 08:04:41 -07:00
|
|
|
if rerr := resp.Body.Close(); err == nil {
|
|
|
|
err = rerr
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
response := newResponse(resp)
|
2020-07-27 02:08:38 -07:00
|
|
|
c.ratemtx.Lock()
|
2020-06-18 08:04:41 -07:00
|
|
|
c.Rate = response.Rate
|
2020-07-27 02:08:38 -07:00
|
|
|
c.ratemtx.Unlock()
|
2020-06-18 08:04:41 -07:00
|
|
|
|
|
|
|
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.
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|