initial commit

This commit is contained in:
bryanl 2014-09-03 10:03:30 -04:00
commit b4bec8f557
30 changed files with 3119 additions and 0 deletions

55
LICENSE.txt Normal file
View File

@ -0,0 +1,55 @@
Copyright (c) 2014 The godo AUTHORS. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
======================
Portions of the client are based on code at:
https://github.com/google/go-github/
Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
OPEN = $(shell which xdg-open || which gnome-open || which open)
cov:
@@gocov test | gocov-html > /tmp/coverage.html
@@${OPEN} /tmp/coverage.html
ci:
go get -d -v -t ./...
go build ./...
go test -v ./...

68
README.md Normal file
View File

@ -0,0 +1,68 @@
# GODO
Godo is a Go client library for accessing the DigitalOcean V2 API.
## Usage
```go
import "github.com/digitaloceancloud/godo"
```
Create a new DigitalOcean client, then use the exposed services to
access different parts of the DigitalOcean API.
### Authentication
Currently, Personal Access Token (PAT) is the only method of
authenticating with the API. You can manage your tokens
at the Digital Ocean Control Panel [Applications Page](https://cloud.digitalocean.com/settings/applications).
You can then use your token to creat a new client:
```go
import "code.google.com/p/goauth2/oauth"
pat := "mytoken"
t := &oauth.Transport{
Token: &oauth.Token{AccessToken: pat},
}
client := godo.NewClient(t.Client())
```
## Examples
[Digital Ocean API Documentation](https://developers.digitalocean.com/v2/)
To list all Droplets your account has access to:
```go
droplets, _, err := client.Droplet.List()
if err != nil {
fmt.Printf("error: %v\n\n", err)
return err
} else {
fmt.Printf("%v\n\n", godo.Stringify(droplets))
}
```
To create a new Droplet:
```go
dropletName := "super-cool-droplet"
createRequest := &godo.DropletCreateRequest{
Name: godo.String(dropletName),
Region: godo.String("nyc2"),
Size: godo.String("512mb"),
Image: godo.Int(3240036), // ubuntu 14.04 64bit
}
newDroplet, _, err := client.Droplet.Create(createRequest)
if err != nil {
fmt.Printf("Something bad happened: %s\n\n", err)
return err
}
```

76
action.go Normal file
View File

@ -0,0 +1,76 @@
package godo
import "fmt"
const (
actionsBasePath = "v2/actions"
// ActionInProgress is an in progress action status
ActionInProgress = "in-progress"
//ActionCompleted is a completed action status
ActionCompleted = "completed"
)
// ImageActionsService handles communition with the image action related methods of the
// DigitalOcean API.
type ActionsService struct {
client *Client
}
type actionsRoot struct {
Actions []Action `json:"actions"`
}
type actionRoot struct {
Event Action `json:"action"`
}
// Action represents a DigitalOcean Action
type Action struct {
ID int `json:"id"`
Status string `json:"status"`
Type string `json:"type"`
StartedAt *Timestamp `json:"started_at"`
CompletedAt *Timestamp `json:"completed_at"`
ResourceID int `json:"resource_id"`
ResourceType string `json:"resource_type"`
}
// List all actions
func (s *ActionsService) List() ([]Action, *Response, error) {
path := actionsBasePath
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Actions, resp, err
}
func (s *ActionsService) Get(id int) (*Action, *Response, error) {
path := fmt.Sprintf("%s/%d", actionsBasePath, id)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
func (a Action) String() string {
return Stringify(a)
}

12
action_request.go Normal file
View File

@ -0,0 +1,12 @@
package godo
// ActionRequest reprents DigitalOcean Action Request
type ActionRequest struct {
Type string `json:"type"`
Params map[string]interface{} `json:"params,omitempty"`
}
// Converts an ActionRequest to a string.
func (d ActionRequest) String() string {
return Stringify(d)
}

16
action_request_test.go Normal file
View File

@ -0,0 +1,16 @@
package godo
import "testing"
func TestActionRequest_String(t *testing.T) {
action := &ActionRequest{
Type: "transfer",
Params: map[string]interface{}{"key-1": "value-1"},
}
stringified := action.String()
expected := `godo.ActionRequest{Type:"transfer", Params:map[key-1:value-1]}`
if expected != stringified {
t.Errorf("Action.Stringify returned %+v, expected %+v", stringified, expected)
}
}

67
action_test.go Normal file
View File

@ -0,0 +1,67 @@
package godo
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestAction_List(t *testing.T) {
setup()
defer teardown()
assert := assert.New(t)
mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"actions": [{"id":1},{"id":2}]}`)
testMethod(t, r, "GET")
})
actions, _, err := client.Actions.List()
assert.NoError(err)
expected := []Action{{ID: 1}, {ID: 2}}
assert.Equal(expected, actions)
}
func TestAction_Get(t *testing.T) {
setup()
defer teardown()
assert := assert.New(t)
mux.HandleFunc("/v2/actions/12345", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"action": {"id":12345}}`)
testMethod(t, r, "GET")
})
action, _, err := client.Actions.Get(12345)
assert.NoError(err)
assert.Equal(12345, action.ID)
}
func TestAction_String(t *testing.T) {
assert := assert.New(t)
pt, err := time.Parse(time.RFC3339, "2014-05-08T20:36:47Z")
assert.NoError(err)
startedAt := &Timestamp{
Time: pt,
}
action := &Action{
ID: 1,
Status: "in-progress",
Type: "transfer",
StartedAt: startedAt,
}
stringified := action.String()
expected := `godo.Action{ID:1, Status:"in-progress", Type:"transfer", ` +
`StartedAt:godo.Timestamp{2014-05-08 20:36:47 +0000 UTC}, ` +
`ResourceID:0, ResourceType:""}`
if expected != stringified {
t.Errorf("Action.Stringify returned %+v, expected %+v", stringified, expected)
}
}

351
doapi.go Normal file
View File

@ -0,0 +1,351 @@
package godo
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"strconv"
"time"
"github.com/google/go-querystring/query"
headerLink "github.com/tent/http-link-go"
)
const (
libraryVersion = "0.1.0"
defaultBaseURL = "https://api.digitalocean.com/"
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
headerRateLimit = "X-RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining"
headerRateReset = "X-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.
Rate Rate
// Services used for communicating with the API
Actions *ActionsService
Domains *DomainsService
Droplet *DropletsService
DropletActions *DropletActionsService
Images *ImagesService
ImageActions *ImageActionsService
Keys *KeysService
Regions *RegionsService
Sizes *SizesService
}
// 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 Digital Ocean response. This wraps the standard http.Response returned from DigitalOcean.
type Response struct {
*http.Response
// These fields provide the page values for paginating through a set of
// results. Any or all of these may be set to the zero value for
// responses that are not part of a paginated set, or for which there
// are no additional pages.
NextPage string
PrevPage string
FirstPage string
LastPage string
// 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
}
// 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 w\hic 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
}
u, err := url.Parse(s)
if err != nil {
return s, err
}
qv, err := query.Values(opt)
if err != nil {
return s, err
}
u.RawQuery = qv.Encode()
return u.String(), nil
}
// NewClient returns a new Digital Ocean API client.
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 = &ActionsService{client: c}
c.Domains = &DomainsService{client: c}
c.Droplet = &DropletsService{client: c}
c.DropletActions = &DropletActionsService{client: c}
c.Images = &ImagesService{client: c}
c.ImageActions = &ImageActionsService{client: c}
c.Keys = &KeysService{client: c}
c.Regions = &RegionsService{client: c}
c.Sizes = &SizesService{client: c}
return c
}
// 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(method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
u := c.BaseURL.ResolveReference(rel)
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", userAgent)
return req, nil
}
// newResponse creates a new Response for the provided http.Response
func newResponse(r *http.Response) *Response {
response := Response{Response: r}
response.populatePageValues()
response.populateRate()
response.populateMonitor()
return &response
}
// populatePageValues parses the HTTP Link response headers and populates the
// various pagination link values in the Response.
func (r *Response) populatePageValues() {
links, err := r.links()
if err == nil {
var l headerLink.Link
var ok bool
l, ok = links["next"]
if ok {
r.NextPage = l.URI
}
l, ok = links["prev"]
if ok {
r.PrevPage = l.URI
}
l, ok = links["first"]
if ok {
r.FirstPage = l.URI
}
l, ok = links["last"]
if ok {
r.LastPage = l.URI
}
}
}
func (r *Response) populateMonitor() {
links, err := r.links()
if err == nil {
link, ok := links["monitor"]
if ok {
r.Monitor = link.URI
}
}
}
func (r *Response) links() (map[string]headerLink.Link, error) {
if linkText, ok := r.Response.Header["Link"]; ok {
links, err := headerLink.Parse(linkText[0])
if err != nil {
return nil, err
}
linkMap := map[string]headerLink.Link{}
for _, link := range links {
linkMap[link.Rel] = link
}
return linkMap, nil
}
return map[string]headerLink.Link{}, nil
}
// 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(req *http.Request, v interface{}) (*Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
response := newResponse(resp)
c.Rate = response.Rate
err = CheckResponse(resp)
if err != nil {
return response, err
}
if v != nil {
if w, ok := v.(io.Writer); ok {
io.Copy(w, resp.Body)
} else {
json.NewDecoder(resp.Body).Decode(v)
}
}
return response, err
}
func (r *ErrorResponse) Error() string {
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 {
json.Unmarshal(data, errorResponse)
}
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()
}

369
doapi_test.go Normal file
View File

@ -0,0 +1,369 @@
package godo
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
)
var (
mux *http.ServeMux
client *Client
server *httptest.Server
)
func setup() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
client = NewClient(nil)
url, _ := url.Parse(server.URL)
client.BaseURL = url
}
func teardown() {
server.Close()
}
func testMethod(t *testing.T, r *http.Request, expected string) {
if expected != r.Method {
t.Errorf("Request method = %v, expected %v", r.Method, expected)
}
}
type values map[string]string
func testFormValues(t *testing.T, r *http.Request, values values) {
expected := url.Values{}
for k, v := range values {
expected.Add(k, v)
}
r.ParseForm()
if !reflect.DeepEqual(expected, r.Form) {
t.Errorf("Request parameters = %v, expected %v", r.Form, expected)
}
}
func testURLParseError(t *testing.T, err error) {
if err == nil {
t.Errorf("Expected error to be returned")
}
if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
t.Errorf("Expected URL parse error, got %+v", err)
}
}
func TestNewClient(t *testing.T) {
c := NewClient(nil)
if c.BaseURL.String() != defaultBaseURL {
t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL.String(), defaultBaseURL)
}
if c.UserAgent != userAgent {
t.Errorf("NewClick UserAgent = %v, expected %v", c.UserAgent, userAgent)
}
}
func TestNewRequest(t *testing.T) {
c := NewClient(nil)
inURL, outURL := "/foo", defaultBaseURL+"foo"
inBody, outBody := &DropletCreateRequest{Name: "l"}, `{"name":"l","region":"","size":"","image":"","ssh_keys":null}`+"\n"
req, _ := c.NewRequest("GET", inURL, inBody)
// test relative URL was expanded
if req.URL.String() != outURL {
t.Errorf("NewRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL)
}
// test body was JSON encoded
body, _ := ioutil.ReadAll(req.Body)
if string(body) != outBody {
t.Errorf("NewRequest(%v)Body = %v, expected %v", inBody, string(body), outBody)
}
// test default user-agent is attached to the request
userAgent := req.Header.Get("User-Agent")
if c.UserAgent != userAgent {
t.Errorf("NewRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent)
}
}
func TestNewRequest_invalidJSON(t *testing.T) {
c := NewClient(nil)
type T struct {
A map[int]interface{}
}
_, err := c.NewRequest("GET", "/", &T{})
if err == nil {
t.Error("Expected error to be returned.")
}
if err, ok := err.(*json.UnsupportedTypeError); !ok {
t.Errorf("Expected a JSON error; got %#v.", err)
}
}
func TestNewRequest_badURL(t *testing.T) {
c := NewClient(nil)
_, err := c.NewRequest("GET", ":", nil)
testURLParseError(t, err)
}
func TestDo(t *testing.T) {
setup()
defer teardown()
type foo struct {
A string
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if m := "GET"; m != r.Method {
t.Errorf("Request method = %v, expected %v", r.Method, m)
}
fmt.Fprint(w, `{"A":"a"}`)
})
req, _ := client.NewRequest("GET", "/", nil)
body := new(foo)
client.Do(req, body)
expected := &foo{"a"}
if !reflect.DeepEqual(body, expected) {
t.Errorf("Response body = %v, expected %v", body, expected)
}
}
func TestDo_httpError(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400)
})
req, _ := client.NewRequest("GET", "/", nil)
_, err := client.Do(req, nil)
if err == nil {
t.Error("Expected HTTP 400 error.")
}
}
// Test handling of an error caused by the internal http client's Do()
// function.
func TestDo_redirectLoop(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
})
req, _ := client.NewRequest("GET", "/", nil)
_, err := client.Do(req, nil)
if err == nil {
t.Error("Expected error to be returned.")
}
if err, ok := err.(*url.Error); !ok {
t.Errorf("Expected a URL error; got %#v.", err)
}
}
func TestCheckResponse(t *testing.T) {
res := &http.Response{
Request: &http.Request{},
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader(`{"message":"m",
"errors": [{"resource": "r", "field": "f", "code": "c"}]}`)),
}
err := CheckResponse(res).(*ErrorResponse)
if err == nil {
t.Fatalf("Expected error response.")
}
expected := &ErrorResponse{
Response: res,
Message: "m",
}
if !reflect.DeepEqual(err, expected) {
t.Errorf("Error = %#v, expected %#v", err, expected)
}
}
// ensure that we properly handle API errors that do not contain a response
// body
func TestCheckResponse_noBody(t *testing.T) {
res := &http.Response{
Request: &http.Request{},
StatusCode: http.StatusBadRequest,
Body: ioutil.NopCloser(strings.NewReader("")),
}
err := CheckResponse(res).(*ErrorResponse)
if err == nil {
t.Errorf("Expected error response.")
}
expected := &ErrorResponse{
Response: res,
}
if !reflect.DeepEqual(err, expected) {
t.Errorf("Error = %#v, expected %#v", err, expected)
}
}
func TestErrorResponse_Error(t *testing.T) {
res := &http.Response{Request: &http.Request{}}
err := ErrorResponse{Message: "m", Response: res}
if err.Error() == "" {
t.Errorf("Expected non-empty ErrorResponse.Error()")
}
}
func TestDo_rateLimit(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerRateLimit, "60")
w.Header().Add(headerRateRemaining, "59")
w.Header().Add(headerRateReset, "1372700873")
})
var expected int
if expected = 0; client.Rate.Limit != expected {
t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
}
if expected = 0; client.Rate.Remaining != expected {
t.Errorf("Client rate remaining = %v, got %v", client.Rate.Remaining, expected)
}
if !client.Rate.Reset.IsZero() {
t.Errorf("Client rate reset not initialized to zero value")
}
req, _ := client.NewRequest("GET", "/", nil)
client.Do(req, nil)
if expected = 60; client.Rate.Limit != expected {
t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
}
if expected = 59; client.Rate.Remaining != expected {
t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected)
}
reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
if client.Rate.Reset.UTC() != reset {
t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset)
}
}
func TestDo_rateLimit_errorResponse(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add(headerRateLimit, "60")
w.Header().Add(headerRateRemaining, "59")
w.Header().Add(headerRateReset, "1372700873")
http.Error(w, "Bad Request", 400)
})
var expected int
req, _ := client.NewRequest("GET", "/", nil)
client.Do(req, nil)
if expected = 60; client.Rate.Limit != expected {
t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected)
}
if expected = 59; client.Rate.Remaining != expected {
t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected)
}
reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
if client.Rate.Reset.UTC() != reset {
t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset)
}
}
func TestResponse_populatePageValues(t *testing.T) {
r := http.Response{
Header: http.Header{
"Link": {`<https://api.digitalocean.com/?page=1>; rel="first",` +
` <https://api.digitalocean.com/?page=2>; rel="prev",` +
` <https://api.digitalocean.com/?page=4>; rel="next",` +
` <https://api.digitalocean.com/?page=5>; rel="last"`,
},
},
}
response := newResponse(&r)
links := map[string]string{
"first": "https://api.digitalocean.com/?page=1",
"prev": "https://api.digitalocean.com/?page=2",
"next": "https://api.digitalocean.com/?page=4",
"last": "https://api.digitalocean.com/?page=5",
}
if expected, got := links["first"], response.FirstPage; expected != got {
t.Errorf("response.FirstPage: %v, expected %v", got, expected)
}
if expected, got := links["prev"], response.PrevPage; expected != got {
t.Errorf("response.PrevPage: %v, expected %v", got, expected)
}
if expected, got := links["next"], response.NextPage; expected != got {
t.Errorf("response.NextPage: %v, expected %v", got, expected)
}
if expected, got := links["last"], response.LastPage; expected != got {
t.Errorf("response.LastPage: %v, expected %v", got, expected)
}
}
func TestResponse_populatePageValues_invalid(t *testing.T) {
r := http.Response{
Header: http.Header{
"Link": {`<https://api.digitalocean.com/?page=1>,` +
`<https://api.digitalocean.com/?page=abc>; rel="first",` +
`https://api.digitalocean.com/?page=2; rel="prev",` +
`<https://api.digitalocean.com/>; rel="next",` +
`<https://api.digitalocean.com/?page=>; rel="last"`,
},
},
}
response := newResponse(&r)
if expected, got := "", response.FirstPage; expected != got {
t.Errorf("response.FirstPage: %v, expected %v", expected, got)
}
if expected, got := "", response.PrevPage; expected != got {
t.Errorf("response.PrevPage: %v, expected %v", expected, got)
}
if expected, got := "", response.NextPage; expected != got {
t.Errorf("response.NextPage: %v, expected %v", expected, got)
}
if expected, got := "", response.LastPage; expected != got {
t.Errorf("response.LastPage: %v, expected %v", expected, got)
}
// more invalid URLs
r = http.Response{
Header: http.Header{
"Link": {`<https://api.digitalocean.com/%?page=2>; rel="first"`},
},
}
}

2
doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package godo is the DigtalOcean API v2 client for Go
package godo

149
domains.go Normal file
View File

@ -0,0 +1,149 @@
package godo
import "fmt"
const domainsBasePath = "v2/domains"
// DomainsService handles communication wit the domain related methods of the
// DigitalOcean API.
type DomainsService struct {
client *Client
}
type DomainRecordRoot struct {
DomainRecord *DomainRecord `json:"domain_record"`
}
type DomainRecordsRoot struct {
DomainRecords []DomainRecord `json:"domain_records"`
}
// DomainRecord represents a DigitalOcean DomainRecord
type DomainRecord struct {
ID int `json:"id,float64,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
Priority int `json:"priority,omitempty"`
Port int `json:"port,omitempty"`
Weight int `json:"weight,omitempty"`
}
type DomainRecordsOptions struct {
ListOptions
}
// Converts a DomainRecord to a string.
func (d DomainRecord) String() string {
return Stringify(d)
}
// DomainRecordEditRequest represents a request to update a domain record.
type DomainRecordEditRequest struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
Priority int `json:"priority,omitempty"`
Port int `json:"port,omitempty"`
Weight int `json:"weight,omitempty"`
}
// Converts a DomainRecordEditRequest to a string.
func (d DomainRecordEditRequest) String() string {
return Stringify(d)
}
// Records returns a slice of DomainRecords for a domain
func (s *DomainsService) Records(domain string, opt *DomainRecordsOptions) ([]DomainRecord, *Response, error) {
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
records := new(DomainRecordsRoot)
resp, err := s.client.Do(req, records)
if err != nil {
return nil, resp, err
}
return records.DomainRecords, resp, err
}
// Record returns the record id from a domain
func (s *DomainsService) Record(domain string, id int) (*DomainRecord, *Response, error) {
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
record := new(DomainRecordRoot)
resp, err := s.client.Do(req, record)
if err != nil {
return nil, resp, err
}
return record.DomainRecord, resp, err
}
// DeleteRecord deletes a record from a domain identified by id
func (s *DomainsService) DeleteRecord(domain string, id int) (*Response, error) {
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// EditRecord edits a record using a DomainRecordEditRequest
func (s *DomainsService) EditRecord(
domain string,
id int,
editRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("PUT", path, editRequest)
if err != nil {
return nil, nil, err
}
d := new(DomainRecord)
resp, err := s.client.Do(req, d)
if err != nil {
return nil, resp, err
}
return d, resp, err
}
// CreateRecord creates a record using a DomainRecordEditRequest
func (s *DomainsService) CreateRecord(
domain string,
createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
req, err := s.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
d := new(DomainRecordRoot)
resp, err := s.client.Do(req, d)
if err != nil {
return nil, resp, err
}
return d.DomainRecord, resp, err
}

196
domains_test.go Normal file
View File

@ -0,0 +1,196 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestDomains_AllRecordsForDomainName(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`)
})
records, _, err := client.Domains.Records("example.com", nil)
if err != nil {
t.Errorf("Domains.List returned error: %v", err)
}
expected := []DomainRecord{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(records, expected) {
t.Errorf("Domains.List returned %+v, expected %+v", records, expected)
}
}
func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) {
perPage := r.URL.Query().Get("per_page")
if perPage != "2" {
t.Fatalf("expected '2', got '%s'", perPage)
}
fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`)
})
dro := &DomainRecordsOptions{ListOptions{PerPage: 2}}
records, _, err := client.Domains.Records("example.com", dro)
if err != nil {
t.Errorf("Domains.List returned error: %v", err)
}
expected := []DomainRecord{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(records, expected) {
t.Errorf("Domains.List returned %+v, expected %+v", records, expected)
}
}
func TestDomains_GetRecordforDomainName(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"domain_record":{"id":1}}`)
})
record, _, err := client.Domains.Record("example.com", 1)
if err != nil {
t.Errorf("Domains.GetRecord returned error: %v", err)
}
expected := &DomainRecord{ID: 1}
if !reflect.DeepEqual(record, expected) {
t.Errorf("Domains.GetRecord returned %+v, expected %+v", record, expected)
}
}
func TestDomains_DeleteRecordForDomainName(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Domains.DeleteRecord("example.com", 1)
if err != nil {
t.Errorf("Domains.RecordDelete returned error: %v", err)
}
}
func TestDomains_CreateRecordForDomainName(t *testing.T) {
setup()
defer teardown()
createRequest := &DomainRecordEditRequest{
Type: "CNAME",
Name: "example",
Data: "@",
Priority: 10,
Port: 10,
Weight: 10,
}
mux.HandleFunc("/v2/domains/example.com/records",
func(w http.ResponseWriter, r *http.Request) {
v := new(DomainRecordEditRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprintf(w, `{"domain_record": {"id":1}}`)
})
record, _, err := client.Domains.CreateRecord("example.com", createRequest)
if err != nil {
t.Errorf("Domains.CreateRecord returned error: %v", err)
}
expected := &DomainRecord{ID: 1}
if !reflect.DeepEqual(record, expected) {
t.Errorf("Domains.CreateRecord returned %+v, expected %+v", record, expected)
}
}
func TestDomains_EditRecordForDomainName(t *testing.T) {
setup()
defer teardown()
editRequest := &DomainRecordEditRequest{
Type: "CNAME",
Name: "example",
Data: "@",
Priority: 10,
Port: 10,
Weight: 10,
}
mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) {
v := new(DomainRecordEditRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "PUT")
if !reflect.DeepEqual(v, editRequest) {
t.Errorf("Request body = %+v, expected %+v", v, editRequest)
}
fmt.Fprintf(w, `{"id":1}`)
})
record, _, err := client.Domains.EditRecord("example.com", 1, editRequest)
if err != nil {
t.Errorf("Domains.EditRecord returned error: %v", err)
}
expected := &DomainRecord{ID: 1}
if !reflect.DeepEqual(record, expected) {
t.Errorf("Domains.EditRecord returned %+v, expected %+v", record, expected)
}
}
func TestDomainRecord_String(t *testing.T) {
record := &DomainRecord{
ID: 1,
Type: "CNAME",
Name: "example",
Data: "@",
Priority: 10,
Port: 10,
Weight: 10,
}
stringified := record.String()
expected := `godo.DomainRecord{ID:1, Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, Weight:10}`
if expected != stringified {
t.Errorf("DomainRecord.String returned %+v, expected %+v", stringified, expected)
}
}
func TestDomainRecordEditRequest_String(t *testing.T) {
record := &DomainRecordEditRequest{
Type: "CNAME",
Name: "example",
Data: "@",
Priority: 10,
Port: 10,
Weight: 10,
}
stringified := record.String()
expected := `godo.DomainRecordEditRequest{Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, Weight:10}`
if expected != stringified {
t.Errorf("DomainRecordEditRequest.String returned %+v, expected %+v", stringified, expected)
}
}

132
droplet_actions.go Normal file
View File

@ -0,0 +1,132 @@
package godo
import (
"fmt"
"net/url"
)
// DropletActionsService handles communication with the droplet action related
// methods of the DigitalOcean API.
type DropletActionsService struct {
client *Client
}
// Shutdown a Droplet
func (s *DropletActionsService) Shutdown(id int) (*Action, *Response, error) {
request := &ActionRequest{Type: "shutdown"}
return s.doAction(id, request)
}
// PowerOff a Droplet
func (s *DropletActionsService) PowerOff(id int) (*Action, *Response, error) {
request := &ActionRequest{Type: "power_off"}
return s.doAction(id, request)
}
// PowerCycle a Droplet
func (s *DropletActionsService) PowerCycle(id int) (*Action, *Response, error) {
request := &ActionRequest{Type: "power_cycle"}
return s.doAction(id, request)
}
// Reboot a Droplet
func (s *DropletActionsService) Reboot(id int) (*Action, *Response, error) {
request := &ActionRequest{Type: "reboot"}
return s.doAction(id, request)
}
// Restore an image to a Droplet
func (s *DropletActionsService) Restore(id, imageID int) (*Action, *Response, error) {
options := map[string]interface{}{
"image": float64(imageID),
}
requestType := "restore"
request := &ActionRequest{
Type: requestType,
Params: options,
}
return s.doAction(id, request)
}
// Resize a Droplet
func (s *DropletActionsService) Resize(id int, sizeSlug string) (*Action, *Response, error) {
options := map[string]interface{}{
"size": sizeSlug,
}
requestType := "resize"
request := &ActionRequest{
Type: requestType,
Params: options,
}
return s.doAction(id, request)
}
// Rename a Droplet
func (s *DropletActionsService) Rename(id int, name string) (*Action, *Response, error) {
options := map[string]interface{}{
"name": name,
}
requestType := "rename"
request := &ActionRequest{
Type: requestType,
Params: options,
}
return s.doAction(id, request)
}
func (s *DropletActionsService) doAction(id int, request *ActionRequest) (*Action, *Response, error) {
path := dropletActionPath(id)
req, err := s.client.NewRequest("POST", path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
// Get an action for a particular droplet by id.
func (s *DropletActionsService) Get(dropletID, actionID int) (*Action, *Response, error) {
path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID)
return s.get(path)
}
// GetByURI gets an action for a particular droplet by id.
func (s *DropletActionsService) GetByURI(rawurl string) (*Action, *Response, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, nil, err
}
return s.get(u.Path)
}
func (s *DropletActionsService) get(path string) (*Action, *Response, error) {
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
func dropletActionPath(dropletID int) string {
return fmt.Sprintf("v2/droplets/%d/actions", dropletID)
}

268
droplet_actions_test.go Normal file
View File

@ -0,0 +1,268 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestDropletActions_Shutdown(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
Type: "shutdown",
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Shutdown(1)
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_PowerOff(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
Type: "power_off",
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.PowerOff(1)
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Reboot(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
Type: "reboot",
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Reboot(1)
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Restore(t *testing.T) {
setup()
defer teardown()
options := map[string]interface{}{
"image": float64(1),
}
request := &ActionRequest{
Type: "restore",
Params: options,
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Restore(1, 1)
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Resize(t *testing.T) {
setup()
defer teardown()
options := map[string]interface{}{
"size": "1024mb",
}
request := &ActionRequest{
Type: "resize",
Params: options,
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Resize(1, "1024mb")
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Rename(t *testing.T) {
setup()
defer teardown()
options := map[string]interface{}{
"name": "Droplet-Name",
}
request := &ActionRequest{
Type: "rename",
Params: options,
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Rename(1, "Droplet-Name")
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_PowerCycle(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
Type: "power_cycle",
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, request) {
t.Errorf("Request body = %+v, expected %+v", v, request)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.PowerCycle(1)
if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected)
}
}
func TestDropletActions_Get(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets/123/actions/456", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.DropletActions.Get(123, 456)
if err != nil {
t.Errorf("DropletActions.Get returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.Get returned %+v, expected %+v", action, expected)
}
}

176
droplets.go Normal file
View File

@ -0,0 +1,176 @@
package godo
import "fmt"
const dropletBasePath = "v2/droplets"
// DropletsService handles communication with the droplet related methods of the
// DigitalOcean API.
type DropletsService struct {
client *Client
}
// Droplet represents a DigitalOcean Droplet
type Droplet struct {
ID int `json:"id,float64,omitempty"`
Name string `json:"name,omitempty"`
Memory int `json:"memory,omitempty"`
Vcpus int `json:"vcpus,omitempty"`
Disk int `json:"disk,omitempty"`
Region *Region `json:"region,omitempty"`
Image *Image `json:"image,omitempty"`
Size *Size `json:"size,omitempty"`
BackupIDs []int `json:"backup_ids,omitempty"`
SnapshotIDs []int `json:"snapshot_ids,omitempty"`
Locked bool `json:"locked,bool,omitempty"`
Status string `json:"status,omitempty"`
Networks *Networks `json:"networks,omitempty"`
ActionIDs []int `json:"action_ids,omitempty"`
}
// Convert Droplet to a string
func (d Droplet) String() string {
return Stringify(d)
}
// DropletRoot represents a Droplet root
type DropletRoot struct {
Droplet *Droplet `json:"droplet"`
Links *Links `json:"links,omitempty"`
}
type dropletsRoot struct {
Droplets []Droplet `json:"droplets"`
}
// DropletCreateRequest represents a request to create a droplet.
type DropletCreateRequest struct {
Name string `json:"name"`
Region string `json:"region"`
Size string `json:"size"`
Image string `json:"image"`
SSHKeys []interface{} `json:"ssh_keys"`
}
func (d DropletCreateRequest) String() string {
return Stringify(d)
}
// Networks represents the droplet's networks
type Networks struct {
V4 []Network `json:"v4,omitempty"`
V6 []Network `json:"v6,omitempty"`
}
// Network represents a DigitalOcean Network
type Network struct {
IPAddress string `json:"ip_address,omitempty"`
Netmask string `json:"netmask,omitempty"`
Gateway string `json:"gateway,omitempty"`
Type string `json:"type,omitempty"`
}
func (n Network) String() string {
return Stringify(n)
}
// Links are extra links for a droplet
type Links struct {
Actions []Link `json:"actions,omitempty"`
}
// Action extracts Link
func (l *Links) Action(action string) *Link {
for _, a := range l.Actions {
if a.Rel == action {
return &a
}
}
return nil
}
// Link represents a link
type Link struct {
ID int `json:"id,omitempty"`
Rel string `json:"rel,omitempty"`
HREF string `json:"href,omitempty"`
}
// List all droplets
func (s *DropletsService) List() ([]Droplet, *Response, error) {
path := dropletBasePath
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
droplets := new(dropletsRoot)
resp, err := s.client.Do(req, droplets)
if err != nil {
return nil, resp, err
}
return droplets.Droplets, resp, err
}
// Get individual droplet
func (s *DropletsService) Get(dropletID int) (*DropletRoot, *Response, error) {
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(DropletRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root, resp, err
}
// Create droplet
func (s *DropletsService) Create(createRequest *DropletCreateRequest) (*DropletRoot, *Response, error) {
path := dropletBasePath
req, err := s.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(DropletRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root, resp, err
}
// Delete droplet
func (s *DropletsService) Delete(dropletID int) (*Response, error) {
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
func (s *DropletsService) dropletActionStatus(uri string) (string, error) {
action, _, err := s.client.DropletActions.GetByURI(uri)
if err != nil {
return "", err
}
return action.Status, nil
}

191
droplets_test.go Normal file
View File

@ -0,0 +1,191 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestDroplets_ListDroplets(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
})
droplets, _, err := client.Droplet.List()
if err != nil {
t.Errorf("Droplets.List returned error: %v", err)
}
expected := []Droplet{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(droplets, expected) {
t.Errorf("Droplets.List returned %+v, expected %+v", droplets, expected)
}
}
func TestDroplets_GetDroplet(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"droplet":{"id":12345}}`)
})
droplets, _, err := client.Droplet.Get(12345)
if err != nil {
t.Errorf("Droplet.Get returned error: %v", err)
}
expected := &DropletRoot{Droplet: &Droplet{ID: 12345}}
if !reflect.DeepEqual(droplets, expected) {
t.Errorf("Droplets.Get returned %+v, expected %+v", droplets, expected)
}
}
func TestDroplets_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &DropletCreateRequest{
Name: "name",
Region: "region",
Size: "size",
Image: "1",
}
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
v := new(DropletCreateRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprintf(w, `{"droplet":{"id":1}}`)
})
droplet, _, err := client.Droplet.Create(createRequest)
if err != nil {
t.Errorf("Droplets.Create returned error: %v", err)
}
expected := &DropletRoot{Droplet: &Droplet{ID: 1}}
if !reflect.DeepEqual(droplet, expected) {
t.Errorf("Droplets.Create returned %+v, expected %+v", droplet, expected)
}
}
func TestDroplets_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Droplet.Delete(12345)
if err != nil {
t.Errorf("Droplet.Delete returned error: %v", err)
}
}
func TestLinks_Actions(t *testing.T) {
setup()
defer teardown()
aLink := Link{ID: 1, Rel: "a", HREF: "http://example.com/a"}
links := Links{
Actions: []Link{
aLink,
Link{ID: 2, Rel: "b", HREF: "http://example.com/b"},
Link{ID: 2, Rel: "c", HREF: "http://example.com/c"},
},
}
link := links.Action("a")
if *link != aLink {
t.Errorf("expected %+v, got %+v", aLink, link)
}
}
func TestNetwork_String(t *testing.T) {
network := &Network{
IPAddress: "192.168.1.2",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
}
stringified := network.String()
expected := `godo.Network{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}`
if expected != stringified {
t.Errorf("Distribution.String returned %+v, expected %+v", stringified, expected)
}
}
func TestDroplet_String(t *testing.T) {
region := &Region{
Slug: "region",
Name: "Region",
Sizes: []string{"1", "2"},
Available: true,
}
image := &Image{
ID: 1,
Name: "Image",
Distribution: "Ubuntu",
Slug: "image",
Public: true,
Regions: []string{"one", "two"},
}
size := &Size{
Slug: "size",
PriceMonthly: 123,
PriceHourly: 456,
Regions: []string{"1", "2"},
}
network := &Network{
IPAddress: "192.168.1.2",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
}
networks := &Networks{
V4: []Network{*network},
}
droplet := &Droplet{
ID: 1,
Name: "droplet",
Memory: 123,
Vcpus: 456,
Disk: 789,
Region: region,
Image: image,
Size: size,
BackupIDs: []int{1},
SnapshotIDs: []int{1},
ActionIDs: []int{1},
Locked: false,
Status: "active",
Networks: networks,
}
stringified := droplet.String()
expected := `godo.Droplet{ID:1, Name:"droplet", Memory:123, Vcpus:456, Disk:789, Region:godo.Region{Slug:"region", Name:"Region", Sizes:["1" "2"], Available:true}, Image:godo.Image{ID:1, Name:"Image", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"]}, Size:godo.Size{Slug:"size", Memory:0, Vcpus:0, Disk:0, PriceMonthly:123, PriceHourly:456, Regions:["1" "2"]}, BackupIDs:[1], SnapshotIDs:[1], Locked:false, Status:"active", Networks:godo.Networks{V4:[godo.Network{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}]}, ActionIDs:[1]}`
if expected != stringified {
t.Errorf("Droplet.String returned %+v, expected %+v", stringified, expected)
}
}

45
image_actions.go Normal file
View File

@ -0,0 +1,45 @@
package godo
import "fmt"
// ImageActionsService handles communition with the image action related methods of the
// DigitalOcean API.
type ImageActionsService struct {
client *Client
}
// Transfer an image
func (i *ImageActionsService) Transfer(imageID int, transferRequest *ActionRequest) (*Action, *Response, error) {
path := fmt.Sprintf("v2/images/%d/actions", imageID)
req, err := i.client.NewRequest("POST", path, transferRequest)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := i.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
// Get an action for a particular image by id.
func (i *ImageActionsService) Get(imageID, actionID int) (*Action, *Response, error) {
path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
req, err := i.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := i.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}

59
image_actions_test.go Normal file
View File

@ -0,0 +1,59 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestImageActions_Transfer(t *testing.T) {
setup()
defer teardown()
transferRequest := &ActionRequest{}
mux.HandleFunc("/v2/images/12345/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, transferRequest) {
t.Errorf("Request body = %+v, expected %+v", v, transferRequest)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
transfer, _, err := client.ImageActions.Transfer(12345, transferRequest)
if err != nil {
t.Errorf("ImageActions.Transfer returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(transfer, expected) {
t.Errorf("ImageActions.Transfer returned %+v, expected %+v", transfer, expected)
}
}
func TestImageActions_Get(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/images/123/actions/456", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.ImageActions.Get(123, 456)
if err != nil {
t.Errorf("ImageActions.Get returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("ImageActions.Get returned %+v, expected %+v", action, expected)
}
}

47
images.go Normal file
View File

@ -0,0 +1,47 @@
package godo
// ImagesService handles communication with the image related methods of the
// DigitalOcean API.
type ImagesService struct {
client *Client
}
// Image represents a DigitalOcean Image
type Image struct {
ID int `json:"id,float64,omitempty"`
Name string `json:"name,omitempty"`
Distribution string `json:"distribution,omitempty"`
Slug string `json:"slug,omitempty"`
Public bool `json:"public,omitempty"`
Regions []string `json:"regions,omitempty"`
}
type imageRoot struct {
Image Image
}
type imagesRoot struct {
Images []Image
}
func (i Image) String() string {
return Stringify(i)
}
// List all sizes
func (s *ImagesService) List() ([]Image, *Response, error) {
path := "v2/images"
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
images := new(imagesRoot)
resp, err := s.client.Do(req, images)
if err != nil {
return nil, resp, err
}
return images.Images, resp, err
}

45
images_test.go Normal file
View File

@ -0,0 +1,45 @@
package godo
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestImages_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
})
images, _, err := client.Images.List()
if err != nil {
t.Errorf("Images.List returned error: %v", err)
}
expected := []Image{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(images, expected) {
t.Errorf("Images.List returned %+v, expected %+v", images, expected)
}
}
func TestImage_String(t *testing.T) {
image := &Image{
ID: 1,
Name: "Image",
Distribution: "Ubuntu",
Slug: "image",
Public: true,
Regions: []string{"one", "two"},
}
stringified := image.String()
expected := `godo.Image{ID:1, Name:"Image", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"]}`
if expected != stringified {
t.Errorf("Image.String returned %+v, expected %+v", stringified, expected)
}
}

121
keys.go Normal file
View File

@ -0,0 +1,121 @@
package godo
import "fmt"
const keysBasePath = "v2/account/keys"
// KeysService handles communication with key related method of the
// DigitalOcean API.
type KeysService struct {
client *Client
}
// Key represents a DigitalOcean Key.
type Key struct {
ID int `json:"id,float64,omitempty"`
Name string `json:"name,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
PublicKey string `json:"public_key,omitempty"`
}
type keysRoot struct {
SSHKeys []Key `json:"ssh_keys"`
}
type keyRoot struct {
SSHKey Key `json:"ssh_key"`
}
func (s Key) String() string {
return Stringify(s)
}
// KeyCreateRequest represents a request to create a new key.
type KeyCreateRequest struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
// List all keys
func (s *KeysService) List() ([]Key, *Response, error) {
req, err := s.client.NewRequest("GET", keysBasePath, nil)
if err != nil {
return nil, nil, err
}
keys := new(keysRoot)
resp, err := s.client.Do(req, keys)
if err != nil {
return nil, resp, err
}
return keys.SSHKeys, resp, err
}
// Performs a get given a path
func (s *KeysService) get(path string) (*Key, *Response, error) {
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(keyRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.SSHKey, resp, err
}
// GetByID gets a Key by id
func (s *KeysService) GetByID(keyID int) (*Key, *Response, error) {
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
return s.get(path)
}
// GetByFingerprint gets a Key by by fingerprint
func (s *KeysService) GetByFingerprint(fingerprint string) (*Key, *Response, error) {
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
return s.get(path)
}
// Create a key using a KeyCreateRequest
func (s *KeysService) Create(createRequest *KeyCreateRequest) (*Key, *Response, error) {
req, err := s.client.NewRequest("POST", keysBasePath, createRequest)
if err != nil {
return nil, nil, err
}
root := new(keyRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.SSHKey, resp, err
}
// Delete key using a path
func (s *KeysService) delete(path string) (*Response, error) {
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// DeleteByID deletes a key by its id
func (s *KeysService) DeleteByID(keyID int) (*Response, error) {
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
return s.delete(path)
}
// DeleteByFingerprint deletes a key by its fingerprint
func (s *KeysService) DeleteByFingerprint(fingerprint string) (*Response, error) {
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
return s.delete(path)
}

144
keys_test.go Normal file
View File

@ -0,0 +1,144 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestKeys_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"ssh_keys":[{"id":1},{"id":2}]} `)
})
keys, _, err := client.Keys.List()
if err != nil {
t.Errorf("Keys.List returned error: %v", err)
}
expected := []Key{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(keys, expected) {
t.Errorf("Keys.List returned %+v, expected %+v", keys, expected)
}
}
func TestKeys_GetByID(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"ssh_key": {"id":12345}}`)
})
keys, _, err := client.Keys.GetByID(12345)
if err != nil {
t.Errorf("Keys.GetByID returned error: %v", err)
}
expected := &Key{ID: 12345}
if !reflect.DeepEqual(keys, expected) {
t.Errorf("Keys.GetByID returned %+v, expected %+v", keys, expected)
}
}
func TestKeys_GetByFingerprint(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"ssh_key": {"fingerprint":"aa:bb:cc"}}`)
})
keys, _, err := client.Keys.GetByFingerprint("aa:bb:cc")
if err != nil {
t.Errorf("Keys.GetByFingerprint returned error: %v", err)
}
expected := &Key{Fingerprint: "aa:bb:cc"}
if !reflect.DeepEqual(keys, expected) {
t.Errorf("Keys.GetByFingerprint returned %+v, expected %+v", keys, expected)
}
}
func TestKeys_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &KeyCreateRequest{
Name: "name",
PublicKey: "ssh-rsa longtextandstuff",
}
mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) {
v := new(KeyCreateRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprintf(w, `{"ssh_key":{"id":1}}`)
})
key, _, err := client.Keys.Create(createRequest)
if err != nil {
t.Errorf("Keys.Create returned error: %v", err)
}
expected := &Key{ID: 1}
if !reflect.DeepEqual(key, expected) {
t.Errorf("Keys.Create returned %+v, expected %+v", key, expected)
}
}
func TestKeys_DestroyByID(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Keys.DeleteByID(12345)
if err != nil {
t.Errorf("Keys.Delete returned error: %v", err)
}
}
func TestKeys_DestroyByFingerprint(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Keys.DeleteByFingerprint("aa:bb:cc")
if err != nil {
t.Errorf("Keys.Delete returned error: %v", err)
}
}
func TestKey_String(t *testing.T) {
key := &Key{
ID: 123,
Name: "Key",
Fingerprint: "fingerprint",
PublicKey: "public key",
}
stringified := key.String()
expected := `godo.Key{ID:123, Name:"Key", Fingerprint:"fingerprint", PublicKey:"public key"}`
if expected != stringified {
t.Errorf("Key.String returned %+v, expected %+v", stringified, expected)
}
}

45
regions.go Normal file
View File

@ -0,0 +1,45 @@
package godo
// RegionsService handles communication with the region related methods of the
// DigitalOcean API.
type RegionsService struct {
client *Client
}
// Region represents a DigitalOcean Region
type Region struct {
Slug string `json:"slug,omitempty"`
Name string `json:"name,omitempty"`
Sizes []string `json:"sizes,omitempty"`
Available bool `json:"available,omitempty`
}
type regionsRoot struct {
Regions []Region
}
type regionRoot struct {
Region Region
}
func (r Region) String() string {
return Stringify(r)
}
// List all regions
func (s *RegionsService) List() ([]Region, *Response, error) {
path := "v2/regions"
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
regions := new(regionsRoot)
resp, err := s.client.Do(req, regions)
if err != nil {
return nil, resp, err
}
return regions.Regions, resp, err
}

43
regions_test.go Normal file
View File

@ -0,0 +1,43 @@
package godo
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestRegions_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`)
})
regions, _, err := client.Regions.List()
if err != nil {
t.Errorf("Regions.List returned error: %v", err)
}
expected := []Region{{Slug: "1"}, {Slug: "2"}}
if !reflect.DeepEqual(regions, expected) {
t.Errorf("Regions.List returned %+v, expected %+v", regions, expected)
}
}
func TestRegion_String(t *testing.T) {
region := &Region{
Slug: "region",
Name: "Region",
Sizes: []string{"1", "2"},
Available: true,
}
stringified := region.String()
expected := `godo.Region{Slug:"region", Name:"Region", Sizes:["1" "2"], Available:true}`
if expected != stringified {
t.Errorf("Region.String returned %+v, expected %+v", stringified, expected)
}
}

44
sizes.go Normal file
View File

@ -0,0 +1,44 @@
package godo
// SizesService handles communication with the size related methods of the
// DigitalOcean API.
type SizesService struct {
client *Client
}
// Size represents a DigitalOcean Size
type Size struct {
Slug string `json:"slug,omitempty"`
Memory int `json:"memory,omitempty"`
Vcpus int `json:"vcpus,omitempty"`
Disk int `json:"disk,omitempty"`
PriceMonthly float64 `json:"price_monthly,omitempty"`
PriceHourly float64 `json:"price_hourly,omitempty"`
Regions []string `json:"regions,omitempty"`
}
func (s Size) String() string {
return Stringify(s)
}
type sizesRoot struct {
Sizes []Size
}
// List all images
func (s *SizesService) List() ([]Size, *Response, error) {
path := "v2/sizes"
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
sizes := new(sizesRoot)
resp, err := s.client.Do(req, sizes)
if err != nil {
return nil, resp, err
}
return sizes.Sizes, resp, err
}

46
sizes_test.go Normal file
View File

@ -0,0 +1,46 @@
package godo
import (
"fmt"
"net/http"
"reflect"
"testing"
)
func TestSizes_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`)
})
sizes, _, err := client.Sizes.List()
if err != nil {
t.Errorf("Sizes.List returned error: %v", err)
}
expected := []Size{{Slug: "1"}, {Slug: "2"}}
if !reflect.DeepEqual(sizes, expected) {
t.Errorf("Sizes.List returned %+v, expected %+v", sizes, expected)
}
}
func TestSize_String(t *testing.T) {
size := &Size{
Slug: "slize",
Memory: 123,
Vcpus: 456,
Disk: 789,
PriceMonthly: 123,
PriceHourly: 456,
Regions: []string{"1", "2"},
}
stringified := size.String()
expected := `godo.Size{Slug:"slize", Memory:123, Vcpus:456, Disk:789, PriceMonthly:123, PriceHourly:456, Regions:["1" "2"]}`
if expected != stringified {
t.Errorf("Size.String returned %+v, expected %+v", stringified, expected)
}
}

84
strings.go Normal file
View File

@ -0,0 +1,84 @@
package godo
import (
"bytes"
"reflect"
"io"
"fmt"
)
var timestampType = reflect.TypeOf(Timestamp{})
// Stringify attempts to create a string representation of Digital Ocean types
func Stringify(message interface{}) string {
var buf bytes.Buffer
v := reflect.ValueOf(message)
stringifyValue(&buf, v)
return buf.String()
}
// stringifyValue was graciously cargoculted from the goprotubuf library
func stringifyValue(w io.Writer, val reflect.Value) {
if val.Kind() == reflect.Ptr && val.IsNil() {
w.Write([]byte("<nil>"))
return
}
v := reflect.Indirect(val)
switch v.Kind() {
case reflect.String:
fmt.Fprintf(w, `"%s"`, v)
case reflect.Slice:
w.Write([]byte{'['})
for i := 0; i < v.Len(); i++ {
if i > 0 {
w.Write([]byte{' '})
}
stringifyValue(w, v.Index(i))
}
w.Write([]byte{']'})
return
case reflect.Struct:
if v.Type().Name() != "" {
w.Write([]byte(v.Type().String()))
}
// special handling of Timestamp values
if v.Type() == timestampType {
fmt.Fprintf(w, "{%s}", v.Interface())
return
}
w.Write([]byte{'{'})
var sep bool
for i := 0; i < v.NumField(); i++ {
fv := v.Field(i)
if fv.Kind() == reflect.Ptr && fv.IsNil() {
continue
}
if fv.Kind() == reflect.Slice && fv.IsNil() {
continue
}
if sep {
w.Write([]byte(", "))
} else {
sep = true
}
w.Write([]byte(v.Type().Field(i).Name))
w.Write([]byte{':'})
stringifyValue(w, fv)
}
w.Write([]byte{'}'})
default:
if v.CanInterface() {
fmt.Fprint(w, v.Interface())
}
}
}

35
timestamp.go Normal file
View File

@ -0,0 +1,35 @@
package godo
import (
"strconv"
"time"
)
// Timestamp represents a time that can be unmarshalled from a JSON string
// formatted as either an RFC3339 or Unix timestamp. All
// exported methods of time.Time can be called on Timestamp.
type Timestamp struct {
time.Time
}
func (t Timestamp) String() string {
return t.Time.String()
}
// UnmarshalJSON implements the json.Unmarshaler interface.
// Time is expected in RFC3339 or Unix format.
func (t *Timestamp) UnmarshalJSON(data []byte) (err error) {
str := string(data)
i, err := strconv.ParseInt(str, 10, 64)
if err == nil {
t.Time = time.Unix(i, 0)
} else {
t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
}
return
}
// Equal reports whether t and u are equal based on time.Equal
func (t Timestamp) Equal(u Timestamp) bool {
return t.Time.Equal(u.Time)
}

176
timestamp_test.go Normal file
View File

@ -0,0 +1,176 @@
package godo
import (
"encoding/json"
"fmt"
"testing"
"time"
)
const (
emptyTimeStr = `"0001-01-01T00:00:00Z"`
referenceTimeStr = `"2006-01-02T15:04:05Z"`
referenceUnixTimeStr = `1136214245`
)
var (
referenceTime = time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC)
unixOrigin = time.Unix(0, 0).In(time.UTC)
)
func TestTimestamp_Marshal(t *testing.T) {
testCases := []struct {
desc string
data Timestamp
want string
wantErr bool
equal bool
}{
{"Reference", Timestamp{referenceTime}, referenceTimeStr, false, true},
{"Empty", Timestamp{}, emptyTimeStr, false, true},
{"Mismatch", Timestamp{}, referenceTimeStr, false, false},
}
for _, tc := range testCases {
out, err := json.Marshal(tc.data)
if gotErr := (err != nil); gotErr != tc.wantErr {
t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
}
got := string(out)
equal := got == tc.want
if (got == tc.want) != tc.equal {
t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
}
}
}
func TestTimestamp_Unmarshal(t *testing.T) {
testCases := []struct {
desc string
data string
want Timestamp
wantErr bool
equal bool
}{
{"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true},
{"ReferenceUnix", `1136214245`, Timestamp{referenceTime}, false, true},
{"Empty", emptyTimeStr, Timestamp{}, false, true},
{"UnixStart", `0`, Timestamp{unixOrigin}, false, true},
{"Mismatch", referenceTimeStr, Timestamp{}, false, false},
{"MismatchUnix", `0`, Timestamp{}, false, false},
{"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false},
}
for _, tc := range testCases {
var got Timestamp
err := json.Unmarshal([]byte(tc.data), &got)
if gotErr := err != nil; gotErr != tc.wantErr {
t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
continue
}
equal := got.Equal(tc.want)
if equal != tc.equal {
t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
}
}
}
func TestTimstamp_MarshalReflexivity(t *testing.T) {
testCases := []struct {
desc string
data Timestamp
}{
{"Reference", Timestamp{referenceTime}},
{"Empty", Timestamp{}},
}
for _, tc := range testCases {
data, err := json.Marshal(tc.data)
if err != nil {
t.Errorf("%s: Marshal err=%v", tc.desc, err)
}
var got Timestamp
err = json.Unmarshal(data, &got)
if !got.Equal(tc.data) {
t.Errorf("%s: %+v != %+v", tc.desc, got, data)
}
}
}
type WrappedTimestamp struct {
A int
Time Timestamp
}
func TestWrappedTimstamp_Marshal(t *testing.T) {
testCases := []struct {
desc string
data WrappedTimestamp
want string
wantErr bool
equal bool
}{
{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, true},
{"Empty", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, emptyTimeStr), false, true},
{"Mismatch", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, false},
}
for _, tc := range testCases {
out, err := json.Marshal(tc.data)
if gotErr := err != nil; gotErr != tc.wantErr {
t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
}
got := string(out)
equal := got == tc.want
if equal != tc.equal {
t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
}
}
}
func TestWrappedTimestamp_Unmarshal(t *testing.T) {
testCases := []struct {
desc string
data string
want WrappedTimestamp
wantErr bool
equal bool
}{
{"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
{"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
{"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true},
{"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true},
{"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false},
{"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false},
{"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false},
}
for _, tc := range testCases {
var got Timestamp
err := json.Unmarshal([]byte(tc.data), &got)
if gotErr := err != nil; gotErr != tc.wantErr {
t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
continue
}
equal := got.Time.Equal(tc.want.Time.Time)
if equal != tc.equal {
t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
}
}
}
func TestWrappedTimestamp_MarshalReflexivity(t *testing.T) {
testCases := []struct {
desc string
data WrappedTimestamp
}{
{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}},
{"Empty", WrappedTimestamp{0, Timestamp{}}},
}
for _, tc := range testCases {
bytes, err := json.Marshal(tc.data)
if err != nil {
t.Errorf("%s: Marshal err=%v", tc.desc, err)
}
var got WrappedTimestamp
err = json.Unmarshal(bytes, &got)
if !got.Time.Equal(tc.data.Time) {
t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data)
}
}
}

47
util/droplet.go Normal file
View File

@ -0,0 +1,47 @@
package util
import (
"fmt"
"time"
"github.com/digitaloceancloud/godo"
)
const (
// activeFailure is the amount of times we can fail before deciding
// the check for active is a total failure. This can help account
// for servers randomly not answering.
activeFailure = 3
)
// WaitForActive waits for a droplet to become active
func WaitForActive(client *godo.Client, monitorURI string) error {
if len(monitorURI) == 0 {
return fmt.Errorf("create had no monitor uri")
}
completed := false
failCount := 0
for !completed {
action, _, err := client.DropletActions.GetByURI(monitorURI)
if err != nil {
if failCount <= activeFailure {
failCount++
continue
}
return err
}
switch action.Status {
case godo.ActionInProgress:
time.Sleep(5 * time.Second)
case godo.ActionCompleted:
completed = true
default:
return fmt.Errorf("unknown status: [%s]", action.Status)
}
}
return nil
}