initial commit
This commit is contained in:
commit
b4bec8f557
|
@ -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.
|
||||
|
|
@ -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 ./...
|
|
@ -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
|
||||
}
|
||||
```
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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"`},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package godo is the DigtalOcean API v2 client for Go
|
||||
package godo
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue