Update digitalocean/godo

This commit is contained in:
Gianluca Arbezzano 2017-03-16 23:06:05 +00:00
parent 2d31ddc26b
commit b41ec45823
49 changed files with 5394 additions and 536 deletions

View File

@ -35,8 +35,8 @@
}, },
{ {
"name": "github.com/digitalocean/godo", "name": "github.com/digitalocean/godo",
"version": "v0.9.0", "version": "v1.0.0",
"revision": "2a0d64a42bb60a95677748a4d5729af6184330b4", "revision": "84099941ba2381607e1b05ffd4822781af86675e",
"packages": [ "packages": [
"." "."
] ]

View File

@ -1,6 +1,7 @@
package provider package provider
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -16,6 +17,7 @@ import (
type DigitalOceanProvider struct { type DigitalOceanProvider struct {
client *godo.Client client *godo.Client
config map[string]string config map[string]string
ctx context.Context
} }
func NewDigitalOceanProvider(c map[string]string) (autoscaler.Provider, error) { func NewDigitalOceanProvider(c map[string]string) (autoscaler.Provider, error) {
@ -27,6 +29,7 @@ func NewDigitalOceanProvider(c map[string]string) (autoscaler.Provider, error) {
p := DigitalOceanProvider{ p := DigitalOceanProvider{
client: client, client: client,
config: c, config: c,
ctx: context.Background(),
} }
return p, nil return p, nil
} }
@ -52,7 +55,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool
Slug: p.config["image"], Slug: p.config["image"],
}, },
} }
droplet, _, err := p.client.Droplets.Create(createRequest) droplet, _, err := p.client.Droplets.Create(p.ctx, createRequest)
responseChannel <- response{ responseChannel <- response{
err: err, err: err,
droplet: droplet, droplet: droplet,
@ -62,7 +65,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool
} }
} else { } else {
// TODO(gianarb): This can not work forever. We need to have proper pagination // TODO(gianarb): This can not work forever. We need to have proper pagination
droplets, _, err := p.client.Droplets.List(&godo.ListOptions{ droplets, _, err := p.client.Droplets.List(p.ctx, &godo.ListOptions{
Page: 1, Page: 1,
PerPage: 500, PerPage: 500,
}) })
@ -80,7 +83,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool
if p.isGoodToBeDeleted(single, serviceId) && ii < target { if p.isGoodToBeDeleted(single, serviceId) && ii < target {
go func() { go func() {
defer wg.Done() defer wg.Done()
_, err := p.client.Droplets.Delete(single.ID) _, err := p.client.Droplets.Delete(p.ctx, single.ID)
responseChannel <- response{ responseChannel <- response{
err: err, err: err,
droplet: &single, droplet: &single,
@ -143,7 +146,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool
func (p DigitalOceanProvider) isGoodToBeDeleted(droplet godo.Droplet, serviceId string) bool { func (p DigitalOceanProvider) isGoodToBeDeleted(droplet godo.Droplet, serviceId string) bool {
if droplet.Status == "active" && strings.Contains(strings.ToUpper(droplet.Name), strings.ToUpper(serviceId)) { if droplet.Status == "active" && strings.Contains(strings.ToUpper(droplet.Name), strings.ToUpper(serviceId)) {
// TODO(gianarb): This can not work forever. We need to have proper pagination // TODO(gianarb): This can not work forever. We need to have proper pagination
actions, _, _ := p.client.Droplets.Actions(droplet.ID, &godo.ListOptions{ actions, _, _ := p.client.Droplets.Actions(p.ctx, droplet.ID, &godo.ListOptions{
Page: 1, Page: 1,
PerPage: 500, PerPage: 500,
}) })

1
vendor/github.com/digitalocean/godo/.gitignore generated vendored Executable file
View File

@ -0,0 +1 @@
vendor/

View File

@ -1,6 +1,5 @@
language: go language: go
go: go:
- 1.3 - 1.7
- 1.4
- tip - tip

View File

@ -1,4 +1,4 @@
Copyright (c) 2014 The godo AUTHORS. All rights reserved. Copyright (c) 2014-2016 The godo AUTHORS. All rights reserved.
MIT License MIT License

View File

@ -65,7 +65,9 @@ createRequest := &godo.DropletCreateRequest{
}, },
} }
newDroplet, _, err := client.Droplets.Create(createRequest) ctx := context.TODO()
newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
if err != nil { if err != nil {
fmt.Printf("Something bad happened: %s\n\n", err) fmt.Printf("Something bad happened: %s\n\n", err)
@ -78,14 +80,14 @@ if err != nil {
If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets: If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets:
```go ```go
func DropletList(client *godo.Client) ([]godo.Droplet, error) { func DropletList(ctx context.Context, client *godo.Client) ([]godo.Droplet, error) {
// create a list to hold our droplets // create a list to hold our droplets
list := []godo.Droplet{} list := []godo.Droplet{}
// create options. initially, these will be blank // create options. initially, these will be blank
opt := &godo.ListOptions{} opt := &godo.ListOptions{}
for { for {
droplets, resp, err := client.Droplets.List(opt) droplets, resp, err := client.Droplets.List(ctx, opt)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,10 +1,12 @@
package godo package godo
import "context"
// AccountService is an interface for interfacing with the Account // AccountService is an interface for interfacing with the Account
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2/#account // See: https://developers.digitalocean.com/documentation/v2/#account
type AccountService interface { type AccountService interface {
Get() (*Account, *Response, error) Get(context.Context) (*Account, *Response, error)
} }
// AccountServiceOp handles communication with the Account related methods of // AccountServiceOp handles communication with the Account related methods of
@ -18,9 +20,12 @@ var _ AccountService = &AccountServiceOp{}
// Account represents a DigitalOcean Account // Account represents a DigitalOcean Account
type Account struct { type Account struct {
DropletLimit int `json:"droplet_limit,omitempty"` DropletLimit int `json:"droplet_limit,omitempty"`
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
UUID string `json:"uuid,omitempty"` UUID string `json:"uuid,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
Status string `json:"status,omitempty"`
StatusMessage string `json:"status_message,omitempty"`
} }
type accountRoot struct { type accountRoot struct {
@ -32,10 +37,11 @@ func (r Account) String() string {
} }
// Get DigitalOcean account info // Get DigitalOcean account info
func (s *AccountServiceOp) Get() (*Account, *Response, error) { func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) {
path := "v2/account" path := "v2/account"
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -17,6 +17,7 @@ func TestAccountGet(t *testing.T) {
response := ` response := `
{ "account": { { "account": {
"droplet_limit": 25, "droplet_limit": 25,
"floating_ip_limit": 25,
"email": "sammy@digitalocean.com", "email": "sammy@digitalocean.com",
"uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", "uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
"email_verified": true "email_verified": true
@ -26,12 +27,12 @@ func TestAccountGet(t *testing.T) {
fmt.Fprint(w, response) fmt.Fprint(w, response)
}) })
acct, _, err := client.Account.Get() acct, _, err := client.Account.Get(ctx)
if err != nil { if err != nil {
t.Errorf("Account.Get returned error: %v", err) t.Errorf("Account.Get returned error: %v", err)
} }
expected := &Account{DropletLimit: 25, Email: "sammy@digitalocean.com", expected := &Account{DropletLimit: 25, FloatingIPLimit: 25, Email: "sammy@digitalocean.com",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true} UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true}
if !reflect.DeepEqual(acct, expected) { if !reflect.DeepEqual(acct, expected) {
t.Errorf("Account.Get returned %+v, expected %+v", acct, expected) t.Errorf("Account.Get returned %+v, expected %+v", acct, expected)
@ -41,13 +42,16 @@ func TestAccountGet(t *testing.T) {
func TestAccountString(t *testing.T) { func TestAccountString(t *testing.T) {
acct := &Account{ acct := &Account{
DropletLimit: 25, DropletLimit: 25,
FloatingIPLimit: 25,
Email: "sammy@digitalocean.com", Email: "sammy@digitalocean.com",
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
EmailVerified: true, EmailVerified: true,
Status: "active",
StatusMessage: "message",
} }
stringified := acct.String() stringified := acct.String()
expected := `godo.Account{DropletLimit:25, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true}` expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}`
if expected != stringified { if expected != stringified {
t.Errorf("Account.String returned %+v, expected %+v", stringified, expected) t.Errorf("Account.String returned %+v, expected %+v", stringified, expected)
} }

View File

@ -1,6 +1,9 @@
package godo package godo
import "fmt" import (
"context"
"fmt"
)
const ( const (
actionsBasePath = "v2/actions" actionsBasePath = "v2/actions"
@ -15,8 +18,8 @@ const (
// ActionsService handles communction with action related methods of the // ActionsService handles communction with action related methods of the
// DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions // DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions
type ActionsService interface { type ActionsService interface {
List(*ListOptions) ([]Action, *Response, error) List(context.Context, *ListOptions) ([]Action, *Response, error)
Get(int) (*Action, *Response, error) Get(context.Context, int) (*Action, *Response, error)
} }
// ActionsServiceOp handles communition with the image action related methods of the // ActionsServiceOp handles communition with the image action related methods of the
@ -33,7 +36,7 @@ type actionsRoot struct {
} }
type actionRoot struct { type actionRoot struct {
Event Action `json:"action"` Event *Action `json:"action"`
} }
// Action represents a DigitalOcean Action // Action represents a DigitalOcean Action
@ -50,14 +53,14 @@ type Action struct {
} }
// List all actions // List all actions
func (s *ActionsServiceOp) List(opt *ListOptions) ([]Action, *Response, error) { func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action, *Response, error) {
path := actionsBasePath path := actionsBasePath
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -75,13 +78,13 @@ func (s *ActionsServiceOp) List(opt *ListOptions) ([]Action, *Response, error) {
} }
// Get an action by ID. // Get an action by ID.
func (s *ActionsServiceOp) Get(id int) (*Action, *Response, error) { func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response, error) {
if id < 1 { if id < 1 {
return nil, nil, NewArgError("id", "cannot be less than 1") return nil, nil, NewArgError("id", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d", actionsBasePath, id) path := fmt.Sprintf("%s/%d", actionsBasePath, id)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -92,7 +95,7 @@ func (s *ActionsServiceOp) Get(id int) (*Action, *Response, error) {
return nil, resp, err return nil, resp, err
} }
return &root.Event, resp, err return root.Event, resp, err
} }
func (a Action) String() string { func (a Action) String() string {

View File

@ -17,7 +17,7 @@ func TestAction_List(t *testing.T) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
}) })
actions, _, err := client.Actions.List(nil) actions, _, err := client.Actions.List(ctx, nil)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }
@ -37,7 +37,7 @@ func TestAction_ListActionMultiplePages(t *testing.T) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
}) })
_, resp, err := client.Actions.List(nil) _, resp, err := client.Actions.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(nil) t.Fatal(nil)
} }
@ -68,7 +68,7 @@ func TestAction_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Actions.List(opt) _, resp, err := client.Actions.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -85,7 +85,7 @@ func TestAction_Get(t *testing.T) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
}) })
action, _, err := client.Actions.Get(12345) action, _, err := client.Actions.Get(ctx, 12345)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)
} }

120
vendor/github.com/digitalocean/godo/certificates.go generated vendored Normal file
View File

@ -0,0 +1,120 @@
package godo
import (
"context"
"path"
)
const certificatesBasePath = "/v2/certificates"
// CertificatesService is an interface for managing certificates with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/v2/#certificates
type CertificatesService interface {
Get(context.Context, string) (*Certificate, *Response, error)
List(context.Context, *ListOptions) ([]Certificate, *Response, error)
Create(context.Context, *CertificateRequest) (*Certificate, *Response, error)
Delete(context.Context, string) (*Response, error)
}
// Certificate represents a DigitalOcean certificate configuration.
type Certificate struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
NotAfter string `json:"not_after,omitempty"`
SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
Created string `json:"created_at,omitempty"`
}
// CertificateRequest represents configuration for a new certificate.
type CertificateRequest struct {
Name string `json:"name,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
LeafCertificate string `json:"leaf_certificate,omitempty"`
CertificateChain string `json:"certificate_chain,omitempty"`
}
type certificateRoot struct {
Certificate *Certificate `json:"certificate"`
}
type certificatesRoot struct {
Certificates []Certificate `json:"certificates"`
Links *Links `json:"links"`
}
// CertificatesServiceOp handles communication with certificates methods of the DigitalOcean API.
type CertificatesServiceOp struct {
client *Client
}
var _ CertificatesService = &CertificatesServiceOp{}
// Get an existing certificate by its identifier.
func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) {
urlStr := path.Join(certificatesBasePath, cID)
req, err := c.client.NewRequest(ctx, "GET", urlStr, nil)
if err != nil {
return nil, nil, err
}
root := new(certificateRoot)
resp, err := c.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Certificate, resp, nil
}
// List all certificates.
func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Certificate, *Response, error) {
urlStr, err := addOptions(certificatesBasePath, opt)
if err != nil {
return nil, nil, err
}
req, err := c.client.NewRequest(ctx, "GET", urlStr, nil)
if err != nil {
return nil, nil, err
}
root := new(certificatesRoot)
resp, err := c.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Certificates, resp, nil
}
// Create a new certificate with provided configuration.
func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) {
req, err := c.client.NewRequest(ctx, "POST", certificatesBasePath, cr)
if err != nil {
return nil, nil, err
}
root := new(certificateRoot)
resp, err := c.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Certificate, resp, nil
}
// Delete a certificate by its identifier.
func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) {
urlStr := path.Join(certificatesBasePath, cID)
req, err := c.client.NewRequest(ctx, "DELETE", urlStr, nil)
if err != nil {
return nil, err
}
return c.client.Do(req, nil)
}

View File

@ -0,0 +1,171 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
var certJSONResponse = `
{
"certificate": {
"id": "892071a0-bb95-49bc-8021-3afd67a210bf",
"name": "web-cert-01",
"not_after": "2017-02-22T00:23:00Z",
"sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
"created_at": "2017-02-08T16:02:37Z"
}
}
`
var certsJSONResponse = `
{
"certificates": [
{
"id": "892071a0-bb95-49bc-8021-3afd67a210bf",
"name": "web-cert-01",
"not_after": "2017-02-22T00:23:00Z",
"sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
"created_at": "2017-02-08T16:02:37Z"
},
{
"id": "992071a0-bb95-49bc-8021-3afd67a210bf",
"name": "web-cert-02",
"not_after": "2017-02-22T00:23:00Z",
"sha1_fingerprint": "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
"created_at": "2017-02-08T16:02:37Z"
}
],
"links": {},
"meta": {
"total": 1
}
}
`
func TestCertificates_Get(t *testing.T) {
setup()
defer teardown()
urlStr := "/v2/certificates"
cID := "892071a0-bb95-49bc-8021-3afd67a210bf"
urlStr = path.Join(urlStr, cID)
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, certJSONResponse)
})
certificate, _, err := client.Certificates.Get(ctx, cID)
if err != nil {
t.Errorf("Certificates.Get returned error: %v", err)
}
expected := &Certificate{
ID: "892071a0-bb95-49bc-8021-3afd67a210bf",
Name: "web-cert-01",
NotAfter: "2017-02-22T00:23:00Z",
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
Created: "2017-02-08T16:02:37Z",
}
assert.Equal(t, expected, certificate)
}
func TestCertificates_List(t *testing.T) {
setup()
defer teardown()
urlStr := "/v2/certificates"
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, certsJSONResponse)
})
certificates, _, err := client.Certificates.List(ctx, nil)
if err != nil {
t.Errorf("Certificates.List returned error: %v", err)
}
expected := []Certificate{
{
ID: "892071a0-bb95-49bc-8021-3afd67a210bf",
Name: "web-cert-01",
NotAfter: "2017-02-22T00:23:00Z",
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
Created: "2017-02-08T16:02:37Z",
},
{
ID: "992071a0-bb95-49bc-8021-3afd67a210bf",
Name: "web-cert-02",
NotAfter: "2017-02-22T00:23:00Z",
SHA1Fingerprint: "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
Created: "2017-02-08T16:02:37Z",
},
}
assert.Equal(t, expected, certificates)
}
func TestCertificates_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &CertificateRequest{
Name: "web-cert-01",
PrivateKey: "-----BEGIN PRIVATE KEY-----",
LeafCertificate: "-----BEGIN CERTIFICATE-----",
CertificateChain: "-----BEGIN CERTIFICATE-----",
}
urlStr := "/v2/certificates"
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
v := new(CertificateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
assert.Equal(t, createRequest, v)
fmt.Fprint(w, certJSONResponse)
})
certificate, _, err := client.Certificates.Create(ctx, createRequest)
if err != nil {
t.Errorf("Certificates.Create returned error: %v", err)
}
expected := &Certificate{
ID: "892071a0-bb95-49bc-8021-3afd67a210bf",
Name: "web-cert-01",
NotAfter: "2017-02-22T00:23:00Z",
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
Created: "2017-02-08T16:02:37Z",
}
assert.Equal(t, expected, certificate)
}
func TestCertificates_Delete(t *testing.T) {
setup()
defer teardown()
cID := "892071a0-bb95-49bc-8021-3afd67a210bf"
urlStr := "/v2/certificates"
urlStr = path.Join(urlStr, cID)
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Certificates.Delete(ctx, cID)
if err != nil {
t.Errorf("Certificates.Delete returned error: %v", err)
}
}

View File

@ -1,6 +1,9 @@
package godo package godo
import "fmt" import (
"context"
"fmt"
)
const domainsBasePath = "v2/domains" const domainsBasePath = "v2/domains"
@ -8,16 +11,16 @@ const domainsBasePath = "v2/domains"
// See: https://developers.digitalocean.com/documentation/v2#domains and // See: https://developers.digitalocean.com/documentation/v2#domains and
// https://developers.digitalocean.com/documentation/v2#domain-records // https://developers.digitalocean.com/documentation/v2#domain-records
type DomainsService interface { type DomainsService interface {
List(*ListOptions) ([]Domain, *Response, error) List(context.Context, *ListOptions) ([]Domain, *Response, error)
Get(string) (*Domain, *Response, error) Get(context.Context, string) (*Domain, *Response, error)
Create(*DomainCreateRequest) (*Domain, *Response, error) Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error)
Delete(string) (*Response, error) Delete(context.Context, string) (*Response, error)
Records(string, *ListOptions) ([]DomainRecord, *Response, error) Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error)
Record(string, int) (*DomainRecord, *Response, error) Record(context.Context, string, int) (*DomainRecord, *Response, error)
DeleteRecord(string, int) (*Response, error) DeleteRecord(context.Context, string, int) (*Response, error)
EditRecord(string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error) EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
CreateRecord(string, *DomainRecordEditRequest) (*DomainRecord, *Response, error) CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
} }
// DomainsServiceOp handles communication with the domain related methods of the // DomainsServiceOp handles communication with the domain related methods of the
@ -88,14 +91,14 @@ func (d Domain) String() string {
} }
// List all domains. // List all domains.
func (s DomainsServiceOp) List(opt *ListOptions) ([]Domain, *Response, error) { func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
path := domainsBasePath path := domainsBasePath
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -113,14 +116,14 @@ func (s DomainsServiceOp) List(opt *ListOptions) ([]Domain, *Response, error) {
} }
// Get individual domain. It requires a non-empty domain name. // Get individual domain. It requires a non-empty domain name.
func (s *DomainsServiceOp) Get(name string) (*Domain, *Response, error) { func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Response, error) {
if len(name) < 1 { if len(name) < 1 {
return nil, nil, NewArgError("name", "cannot be an empty string") return nil, nil, NewArgError("name", "cannot be an empty string")
} }
path := fmt.Sprintf("%s/%s", domainsBasePath, name) path := fmt.Sprintf("%s/%s", domainsBasePath, name)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -135,14 +138,14 @@ func (s *DomainsServiceOp) Get(name string) (*Domain, *Response, error) {
} }
// Create a new domain // Create a new domain
func (s *DomainsServiceOp) Create(createRequest *DomainCreateRequest) (*Domain, *Response, error) { func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCreateRequest) (*Domain, *Response, error) {
if createRequest == nil { if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil") return nil, nil, NewArgError("createRequest", "cannot be nil")
} }
path := domainsBasePath path := domainsBasePath
req, err := s.client.NewRequest("POST", path, createRequest) req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -156,14 +159,14 @@ func (s *DomainsServiceOp) Create(createRequest *DomainCreateRequest) (*Domain,
} }
// Delete domain // Delete domain
func (s *DomainsServiceOp) Delete(name string) (*Response, error) { func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
if len(name) < 1 { if len(name) < 1 {
return nil, NewArgError("name", "cannot be an empty string") return nil, NewArgError("name", "cannot be an empty string")
} }
path := fmt.Sprintf("%s/%s", domainsBasePath, name) path := fmt.Sprintf("%s/%s", domainsBasePath, name)
req, err := s.client.NewRequest("DELETE", path, nil) req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -184,7 +187,7 @@ func (d DomainRecordEditRequest) String() string {
} }
// Records returns a slice of DomainRecords for a domain // Records returns a slice of DomainRecords for a domain
func (s *DomainsServiceOp) Records(domain string, opt *ListOptions) ([]DomainRecord, *Response, error) { func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
if len(domain) < 1 { if len(domain) < 1 {
return nil, nil, NewArgError("domain", "cannot be an empty string") return nil, nil, NewArgError("domain", "cannot be an empty string")
} }
@ -195,7 +198,7 @@ func (s *DomainsServiceOp) Records(domain string, opt *ListOptions) ([]DomainRec
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -213,7 +216,7 @@ func (s *DomainsServiceOp) Records(domain string, opt *ListOptions) ([]DomainRec
} }
// Record returns the record id from a domain // Record returns the record id from a domain
func (s *DomainsServiceOp) Record(domain string, id int) (*DomainRecord, *Response, error) { func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*DomainRecord, *Response, error) {
if len(domain) < 1 { if len(domain) < 1 {
return nil, nil, NewArgError("domain", "cannot be an empty string") return nil, nil, NewArgError("domain", "cannot be an empty string")
} }
@ -224,7 +227,7 @@ func (s *DomainsServiceOp) Record(domain string, id int) (*DomainRecord, *Respon
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id) path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -239,7 +242,7 @@ func (s *DomainsServiceOp) Record(domain string, id int) (*DomainRecord, *Respon
} }
// DeleteRecord deletes a record from a domain identified by id // DeleteRecord deletes a record from a domain identified by id
func (s *DomainsServiceOp) DeleteRecord(domain string, id int) (*Response, error) { func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
if len(domain) < 1 { if len(domain) < 1 {
return nil, NewArgError("domain", "cannot be an empty string") return nil, NewArgError("domain", "cannot be an empty string")
} }
@ -250,7 +253,7 @@ func (s *DomainsServiceOp) DeleteRecord(domain string, id int) (*Response, error
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id) path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("DELETE", path, nil) req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -261,7 +264,7 @@ func (s *DomainsServiceOp) DeleteRecord(domain string, id int) (*Response, error
} }
// EditRecord edits a record using a DomainRecordEditRequest // EditRecord edits a record using a DomainRecordEditRequest
func (s *DomainsServiceOp) EditRecord( func (s *DomainsServiceOp) EditRecord(ctx context.Context,
domain string, domain string,
id int, id int,
editRequest *DomainRecordEditRequest, editRequest *DomainRecordEditRequest,
@ -280,7 +283,7 @@ func (s *DomainsServiceOp) EditRecord(
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id) path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
req, err := s.client.NewRequest("PUT", path, editRequest) req, err := s.client.NewRequest(ctx, "PUT", path, editRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -295,7 +298,7 @@ func (s *DomainsServiceOp) EditRecord(
} }
// CreateRecord creates a record using a DomainRecordEditRequest // CreateRecord creates a record using a DomainRecordEditRequest
func (s *DomainsServiceOp) CreateRecord( func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
domain string, domain string,
createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) { createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
if len(domain) < 1 { if len(domain) < 1 {
@ -307,7 +310,7 @@ func (s *DomainsServiceOp) CreateRecord(
} }
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain) path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
req, err := s.client.NewRequest("POST", path, createRequest) req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -17,7 +17,7 @@ func TestDomains_ListDomains(t *testing.T) {
fmt.Fprint(w, `{"domains": [{"name":"foo.com"},{"name":"bar.com"}]}`) fmt.Fprint(w, `{"domains": [{"name":"foo.com"},{"name":"bar.com"}]}`)
}) })
domains, _, err := client.Domains.List(nil) domains, _, err := client.Domains.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Domains.List returned error: %v", err) t.Errorf("Domains.List returned error: %v", err)
} }
@ -37,7 +37,7 @@ func TestDomains_ListDomainsMultiplePages(t *testing.T) {
fmt.Fprint(w, `{"domains": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/domains/?page=2"}}}`) fmt.Fprint(w, `{"domains": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/domains/?page=2"}}}`)
}) })
_, resp, err := client.Domains.List(nil) _, resp, err := client.Domains.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -68,7 +68,7 @@ func TestDomains_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Domains.List(opt) _, resp, err := client.Domains.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -85,7 +85,7 @@ func TestDomains_GetDomain(t *testing.T) {
fmt.Fprint(w, `{"domain":{"name":"example.com"}}`) fmt.Fprint(w, `{"domain":{"name":"example.com"}}`)
}) })
domains, _, err := client.Domains.Get("example.com") domains, _, err := client.Domains.Get(ctx, "example.com")
if err != nil { if err != nil {
t.Errorf("domain.Get returned error: %v", err) t.Errorf("domain.Get returned error: %v", err)
} }
@ -120,7 +120,7 @@ func TestDomains_Create(t *testing.T) {
fmt.Fprint(w, `{"domain":{"name":"example.com"}}`) fmt.Fprint(w, `{"domain":{"name":"example.com"}}`)
}) })
domain, _, err := client.Domains.Create(createRequest) domain, _, err := client.Domains.Create(ctx, createRequest)
if err != nil { if err != nil {
t.Errorf("Domains.Create returned error: %v", err) t.Errorf("Domains.Create returned error: %v", err)
} }
@ -139,7 +139,7 @@ func TestDomains_Destroy(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Domains.Delete("example.com") _, err := client.Domains.Delete(ctx, "example.com")
if err != nil { if err != nil {
t.Errorf("Domains.Delete returned error: %v", err) t.Errorf("Domains.Delete returned error: %v", err)
} }
@ -154,7 +154,7 @@ func TestDomains_AllRecordsForDomainName(t *testing.T) {
fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`)
}) })
records, _, err := client.Domains.Records("example.com", nil) records, _, err := client.Domains.Records(ctx, "example.com", nil)
if err != nil { if err != nil {
t.Errorf("Domains.List returned error: %v", err) t.Errorf("Domains.List returned error: %v", err)
} }
@ -179,7 +179,7 @@ func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) {
}) })
dro := &ListOptions{PerPage: 2} dro := &ListOptions{PerPage: 2}
records, _, err := client.Domains.Records("example.com", dro) records, _, err := client.Domains.Records(ctx, "example.com", dro)
if err != nil { if err != nil {
t.Errorf("Domains.List returned error: %v", err) t.Errorf("Domains.List returned error: %v", err)
} }
@ -199,7 +199,7 @@ func TestDomains_GetRecordforDomainName(t *testing.T) {
fmt.Fprint(w, `{"domain_record":{"id":1}}`) fmt.Fprint(w, `{"domain_record":{"id":1}}`)
}) })
record, _, err := client.Domains.Record("example.com", 1) record, _, err := client.Domains.Record(ctx, "example.com", 1)
if err != nil { if err != nil {
t.Errorf("Domains.GetRecord returned error: %v", err) t.Errorf("Domains.GetRecord returned error: %v", err)
} }
@ -218,7 +218,7 @@ func TestDomains_DeleteRecordForDomainName(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Domains.DeleteRecord("example.com", 1) _, err := client.Domains.DeleteRecord(ctx, "example.com", 1)
if err != nil { if err != nil {
t.Errorf("Domains.RecordDelete returned error: %v", err) t.Errorf("Domains.RecordDelete returned error: %v", err)
} }
@ -254,7 +254,7 @@ func TestDomains_CreateRecordForDomainName(t *testing.T) {
fmt.Fprintf(w, `{"domain_record": {"id":1}}`) fmt.Fprintf(w, `{"domain_record": {"id":1}}`)
}) })
record, _, err := client.Domains.CreateRecord("example.com", createRequest) record, _, err := client.Domains.CreateRecord(ctx, "example.com", createRequest)
if err != nil { if err != nil {
t.Errorf("Domains.CreateRecord returned error: %v", err) t.Errorf("Domains.CreateRecord returned error: %v", err)
} }
@ -293,7 +293,7 @@ func TestDomains_EditRecordForDomainName(t *testing.T) {
fmt.Fprintf(w, `{"id":1}`) fmt.Fprintf(w, `{"id":1}`)
}) })
record, _, err := client.Domains.EditRecord("example.com", 1, editRequest) record, _, err := client.Domains.EditRecord(ctx, "example.com", 1, editRequest)
if err != nil { if err != nil {
t.Errorf("Domains.EditRecord returned error: %v", err) t.Errorf("Domains.EditRecord returned error: %v", err)
} }

View File

@ -1,6 +1,7 @@
package godo package godo
import ( import (
"context"
"fmt" "fmt"
"net/url" "net/url"
) )
@ -8,32 +9,42 @@ import (
// ActionRequest reprents DigitalOcean Action Request // ActionRequest reprents DigitalOcean Action Request
type ActionRequest map[string]interface{} type ActionRequest map[string]interface{}
// DropletActionsService is an interface for interfacing with the droplet actions // DropletActionsService is an interface for interfacing with the Droplet actions
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#droplet-actions // See: https://developers.digitalocean.com/documentation/v2#droplet-actions
type DropletActionsService interface { type DropletActionsService interface {
Shutdown(int) (*Action, *Response, error) Shutdown(context.Context, int) (*Action, *Response, error)
PowerOff(int) (*Action, *Response, error) ShutdownByTag(context.Context, string) (*Action, *Response, error)
PowerOn(int) (*Action, *Response, error) PowerOff(context.Context, int) (*Action, *Response, error)
PowerCycle(int) (*Action, *Response, error) PowerOffByTag(context.Context, string) (*Action, *Response, error)
Reboot(int) (*Action, *Response, error) PowerOn(context.Context, int) (*Action, *Response, error)
Restore(int, int) (*Action, *Response, error) PowerOnByTag(context.Context, string) (*Action, *Response, error)
Resize(int, string, bool) (*Action, *Response, error) PowerCycle(context.Context, int) (*Action, *Response, error)
Rename(int, string) (*Action, *Response, error) PowerCycleByTag(context.Context, string) (*Action, *Response, error)
Snapshot(int, string) (*Action, *Response, error) Reboot(context.Context, int) (*Action, *Response, error)
DisableBackups(int) (*Action, *Response, error) Restore(context.Context, int, int) (*Action, *Response, error)
PasswordReset(int) (*Action, *Response, error) Resize(context.Context, int, string, bool) (*Action, *Response, error)
RebuildByImageID(int, int) (*Action, *Response, error) Rename(context.Context, int, string) (*Action, *Response, error)
RebuildByImageSlug(int, string) (*Action, *Response, error) Snapshot(context.Context, int, string) (*Action, *Response, error)
ChangeKernel(int, int) (*Action, *Response, error) SnapshotByTag(context.Context, string, string) (*Action, *Response, error)
EnableIPv6(int) (*Action, *Response, error) EnableBackups(context.Context, int) (*Action, *Response, error)
EnablePrivateNetworking(int) (*Action, *Response, error) EnableBackupsByTag(context.Context, string) (*Action, *Response, error)
Upgrade(int) (*Action, *Response, error) DisableBackups(context.Context, int) (*Action, *Response, error)
Get(int, int) (*Action, *Response, error) DisableBackupsByTag(context.Context, string) (*Action, *Response, error)
GetByURI(string) (*Action, *Response, error) PasswordReset(context.Context, int) (*Action, *Response, error)
RebuildByImageID(context.Context, int, int) (*Action, *Response, error)
RebuildByImageSlug(context.Context, int, string) (*Action, *Response, error)
ChangeKernel(context.Context, int, int) (*Action, *Response, error)
EnableIPv6(context.Context, int) (*Action, *Response, error)
EnableIPv6ByTag(context.Context, string) (*Action, *Response, error)
EnablePrivateNetworking(context.Context, int) (*Action, *Response, error)
EnablePrivateNetworkingByTag(context.Context, string) (*Action, *Response, error)
Upgrade(context.Context, int) (*Action, *Response, error)
Get(context.Context, int, int) (*Action, *Response, error)
GetByURI(context.Context, string) (*Action, *Response, error)
} }
// DropletActionsServiceOp handles communication with the droplet action related // DropletActionsServiceOp handles communication with the Droplet action related
// methods of the DigitalOcean API. // methods of the DigitalOcean API.
type DropletActionsServiceOp struct { type DropletActionsServiceOp struct {
client *Client client *Client
@ -42,125 +53,189 @@ type DropletActionsServiceOp struct {
var _ DropletActionsService = &DropletActionsServiceOp{} var _ DropletActionsService = &DropletActionsServiceOp{}
// Shutdown a Droplet // Shutdown a Droplet
func (s *DropletActionsServiceOp) Shutdown(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "shutdown"} request := &ActionRequest{"type": "shutdown"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
}
// ShutdownByTag shuts down Droplets matched by a Tag.
func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "shutdown"}
return s.doActionByTag(ctx, tag, request)
} }
// PowerOff a Droplet // PowerOff a Droplet
func (s *DropletActionsServiceOp) PowerOff(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) PowerOff(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_off"} request := &ActionRequest{"type": "power_off"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
}
// PowerOffByTag powers off Droplets matched by a Tag.
func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_off"}
return s.doActionByTag(ctx, tag, request)
} }
// PowerOn a Droplet // PowerOn a Droplet
func (s *DropletActionsServiceOp) PowerOn(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) PowerOn(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_on"} request := &ActionRequest{"type": "power_on"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
}
// PowerOnByTag powers on Droplets matched by a Tag.
func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_on"}
return s.doActionByTag(ctx, tag, request)
} }
// PowerCycle a Droplet // PowerCycle a Droplet
func (s *DropletActionsServiceOp) PowerCycle(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) PowerCycle(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_cycle"} request := &ActionRequest{"type": "power_cycle"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
}
// PowerCycleByTag power cycles Droplets matched by a Tag.
func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_cycle"}
return s.doActionByTag(ctx, tag, request)
} }
// Reboot a Droplet // Reboot a Droplet
func (s *DropletActionsServiceOp) Reboot(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Reboot(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "reboot"} request := &ActionRequest{"type": "reboot"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// Restore an image to a Droplet // Restore an image to a Droplet
func (s *DropletActionsServiceOp) Restore(id, imageID int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Restore(ctx context.Context, id, imageID int) (*Action, *Response, error) {
requestType := "restore" requestType := "restore"
request := &ActionRequest{ request := &ActionRequest{
"type": requestType, "type": requestType,
"image": float64(imageID), "image": float64(imageID),
} }
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// Resize a Droplet // Resize a Droplet
func (s *DropletActionsServiceOp) Resize(id int, sizeSlug string, resizeDisk bool) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Resize(ctx context.Context, id int, sizeSlug string, resizeDisk bool) (*Action, *Response, error) {
requestType := "resize" requestType := "resize"
request := &ActionRequest{ request := &ActionRequest{
"type": requestType, "type": requestType,
"size": sizeSlug, "size": sizeSlug,
"disk": resizeDisk, "disk": resizeDisk,
} }
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// Rename a Droplet // Rename a Droplet
func (s *DropletActionsServiceOp) Rename(id int, name string) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Rename(ctx context.Context, id int, name string) (*Action, *Response, error) {
requestType := "rename" requestType := "rename"
request := &ActionRequest{ request := &ActionRequest{
"type": requestType, "type": requestType,
"name": name, "name": name,
} }
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// Snapshot a Droplet. // Snapshot a Droplet.
func (s *DropletActionsServiceOp) Snapshot(id int, name string) (*Action, *Response, error) { func (s *DropletActionsServiceOp) Snapshot(ctx context.Context, id int, name string) (*Action, *Response, error) {
requestType := "snapshot" requestType := "snapshot"
request := &ActionRequest{ request := &ActionRequest{
"type": requestType, "type": requestType,
"name": name, "name": name,
} }
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// DisableBackups disables backups for a droplet. // SnapshotByTag snapshots Droplets matched by a Tag.
func (s *DropletActionsServiceOp) DisableBackups(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) (*Action, *Response, error) {
requestType := "snapshot"
request := &ActionRequest{
"type": requestType,
"name": name,
}
return s.doActionByTag(ctx, tag, request)
}
// EnableBackups enables backups for a Droplet.
func (s *DropletActionsServiceOp) EnableBackups(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_backups"}
return s.doAction(ctx, id, request)
}
// EnableBackupsByTag enables backups for Droplets matched by a Tag.
func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_backups"}
return s.doActionByTag(ctx, tag, request)
}
// DisableBackups disables backups for a Droplet.
func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "disable_backups"} request := &ActionRequest{"type": "disable_backups"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// PasswordReset resets the password for a droplet. // DisableBackupsByTag disables backups for Droplet matched by a Tag.
func (s *DropletActionsServiceOp) PasswordReset(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "disable_backups"}
return s.doActionByTag(ctx, tag, request)
}
// PasswordReset resets the password for a Droplet.
func (s *DropletActionsServiceOp) PasswordReset(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "password_reset"} request := &ActionRequest{"type": "password_reset"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// RebuildByImageID rebuilds a droplet droplet from an image with a given id. // RebuildByImageID rebuilds a Droplet from an image with a given id.
func (s *DropletActionsServiceOp) RebuildByImageID(id, imageID int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) RebuildByImageID(ctx context.Context, id, imageID int) (*Action, *Response, error) {
request := &ActionRequest{"type": "rebuild", "image": imageID} request := &ActionRequest{"type": "rebuild", "image": imageID}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// RebuildByImageSlug rebuilds a droplet from an image with a given slug. // RebuildByImageSlug rebuilds a Droplet from an Image matched by a given Slug.
func (s *DropletActionsServiceOp) RebuildByImageSlug(id int, slug string) (*Action, *Response, error) { func (s *DropletActionsServiceOp) RebuildByImageSlug(ctx context.Context, id int, slug string) (*Action, *Response, error) {
request := &ActionRequest{"type": "rebuild", "image": slug} request := &ActionRequest{"type": "rebuild", "image": slug}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// ChangeKernel changes the kernel for a droplet. // ChangeKernel changes the kernel for a Droplet.
func (s *DropletActionsServiceOp) ChangeKernel(id, kernelID int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) ChangeKernel(ctx context.Context, id, kernelID int) (*Action, *Response, error) {
request := &ActionRequest{"type": "change_kernel", "kernel": kernelID} request := &ActionRequest{"type": "change_kernel", "kernel": kernelID}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// EnableIPv6 enables IPv6 for a droplet. // EnableIPv6 enables IPv6 for a Droplet.
func (s *DropletActionsServiceOp) EnableIPv6(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_ipv6"} request := &ActionRequest{"type": "enable_ipv6"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// EnablePrivateNetworking enables private networking for a droplet. // EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag.
func (s *DropletActionsServiceOp) EnablePrivateNetworking(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_ipv6"}
return s.doActionByTag(ctx, tag, request)
}
// EnablePrivateNetworking enables private networking for a Droplet.
func (s *DropletActionsServiceOp) EnablePrivateNetworking(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_private_networking"} request := &ActionRequest{"type": "enable_private_networking"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
// Upgrade a droplet. // EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag.
func (s *DropletActionsServiceOp) Upgrade(id int) (*Action, *Response, error) { func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_private_networking"}
return s.doActionByTag(ctx, tag, request)
}
// Upgrade a Droplet.
func (s *DropletActionsServiceOp) Upgrade(ctx context.Context, id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "upgrade"} request := &ActionRequest{"type": "upgrade"}
return s.doAction(id, request) return s.doAction(ctx, id, request)
} }
func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Action, *Response, error) { func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request *ActionRequest) (*Action, *Response, error) {
if id < 1 { if id < 1 {
return nil, nil, NewArgError("id", "cannot be less than 1") return nil, nil, NewArgError("id", "cannot be less than 1")
} }
@ -171,7 +246,7 @@ func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Act
path := dropletActionPath(id) path := dropletActionPath(id)
req, err := s.client.NewRequest("POST", path, request) req, err := s.client.NewRequest(ctx, "POST", path, request)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -182,11 +257,36 @@ func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Act
return nil, resp, err return nil, resp, err
} }
return &root.Event, resp, err return root.Event, resp, err
} }
// Get an action for a particular droplet by id. func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) (*Action, *Response, error) {
func (s *DropletActionsServiceOp) Get(dropletID, actionID int) (*Action, *Response, error) { if tag == "" {
return nil, nil, NewArgError("tag", "cannot be empty")
}
if request == nil {
return nil, nil, NewArgError("request", "request can't be nil")
}
path := dropletActionPathByTag(tag)
req, err := s.client.NewRequest(ctx, "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 *DropletActionsServiceOp) Get(ctx context.Context, dropletID, actionID int) (*Action, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
@ -196,22 +296,22 @@ func (s *DropletActionsServiceOp) Get(dropletID, actionID int) (*Action, *Respon
} }
path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID) path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID)
return s.get(path) return s.get(ctx, path)
} }
// GetByURI gets an action for a particular droplet by id. // GetByURI gets an action for a particular Droplet by id.
func (s *DropletActionsServiceOp) GetByURI(rawurl string) (*Action, *Response, error) { func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) (*Action, *Response, error) {
u, err := url.Parse(rawurl) u, err := url.Parse(rawurl)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return s.get(u.Path) return s.get(ctx, u.Path)
} }
func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) { func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -222,10 +322,14 @@ func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) {
return nil, resp, err return nil, resp, err
} }
return &root.Event, resp, err return root.Event, resp, err
} }
func dropletActionPath(dropletID int) string { func dropletActionPath(dropletID int) string {
return fmt.Sprintf("v2/droplets/%d/actions", dropletID) return fmt.Sprintf("v2/droplets/%d/actions", dropletID)
} }
func dropletActionPathByTag(tag string) string {
return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag)
}

View File

@ -31,7 +31,7 @@ func TestDropletActions_Shutdown(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.Shutdown(1) action, _, err := client.DropletActions.Shutdown(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.Shutdown returned error: %v", err) t.Errorf("DropletActions.Shutdown returned error: %v", err)
} }
@ -42,6 +42,44 @@ func TestDropletActions_Shutdown(t *testing.T) {
} }
} }
func TestDropletActions_ShutdownByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "shutdown",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.ShutdownByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.ShutdownByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.ShutdownByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.ShutdownByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_PowerOff(t *testing.T) { func TestDropletAction_PowerOff(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -65,7 +103,7 @@ func TestDropletAction_PowerOff(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.PowerOff(1) action, _, err := client.DropletActions.PowerOff(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.PowerOff returned error: %v", err) t.Errorf("DropletActions.PowerOff returned error: %v", err)
} }
@ -76,6 +114,44 @@ func TestDropletAction_PowerOff(t *testing.T) {
} }
} }
func TestDropletAction_PowerOffByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "power_off",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.PowerOffByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.PowerOffByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.PowerOffByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.PoweroffByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_PowerOn(t *testing.T) { func TestDropletAction_PowerOn(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -99,7 +175,7 @@ func TestDropletAction_PowerOn(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.PowerOn(1) action, _, err := client.DropletActions.PowerOn(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.PowerOn returned error: %v", err) t.Errorf("DropletActions.PowerOn returned error: %v", err)
} }
@ -110,6 +186,43 @@ func TestDropletAction_PowerOn(t *testing.T) {
} }
} }
func TestDropletAction_PowerOnByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "power_on",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.PowerOnByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.PowerOnByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.PowerOnByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.PowerOnByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Reboot(t *testing.T) { func TestDropletAction_Reboot(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -134,7 +247,7 @@ func TestDropletAction_Reboot(t *testing.T) {
}) })
action, _, err := client.DropletActions.Reboot(1) action, _, err := client.DropletActions.Reboot(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.Reboot returned error: %v", err) t.Errorf("DropletActions.Reboot returned error: %v", err)
} }
@ -171,7 +284,7 @@ func TestDropletAction_Restore(t *testing.T) {
}) })
action, _, err := client.DropletActions.Restore(1, 1) action, _, err := client.DropletActions.Restore(ctx, 1, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.Restore returned error: %v", err) t.Errorf("DropletActions.Restore returned error: %v", err)
} }
@ -209,7 +322,7 @@ func TestDropletAction_Resize(t *testing.T) {
}) })
action, _, err := client.DropletActions.Resize(1, "1024mb", true) action, _, err := client.DropletActions.Resize(ctx, 1, "1024mb", true)
if err != nil { if err != nil {
t.Errorf("DropletActions.Resize returned error: %v", err) t.Errorf("DropletActions.Resize returned error: %v", err)
} }
@ -245,7 +358,7 @@ func TestDropletAction_Rename(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.Rename(1, "Droplet-Name") action, _, err := client.DropletActions.Rename(ctx, 1, "Droplet-Name")
if err != nil { if err != nil {
t.Errorf("DropletActions.Rename returned error: %v", err) t.Errorf("DropletActions.Rename returned error: %v", err)
} }
@ -280,7 +393,7 @@ func TestDropletAction_PowerCycle(t *testing.T) {
}) })
action, _, err := client.DropletActions.PowerCycle(1) action, _, err := client.DropletActions.PowerCycle(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.PowerCycle returned error: %v", err) t.Errorf("DropletActions.PowerCycle returned error: %v", err)
} }
@ -291,6 +404,45 @@ func TestDropletAction_PowerCycle(t *testing.T) {
} }
} }
func TestDropletAction_PowerCycleByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "power_cycle",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.PowerCycleByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.PowerCycleByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.PowerCycleByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.PowerCycleByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Snapshot(t *testing.T) { func TestDropletAction_Snapshot(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -316,7 +468,7 @@ func TestDropletAction_Snapshot(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.Snapshot(1, "Image-Name") action, _, err := client.DropletActions.Snapshot(ctx, 1, "Image-Name")
if err != nil { if err != nil {
t.Errorf("DropletActions.Snapshot returned error: %v", err) t.Errorf("DropletActions.Snapshot returned error: %v", err)
} }
@ -327,6 +479,120 @@ func TestDropletAction_Snapshot(t *testing.T) {
} }
} }
func TestDropletAction_SnapshotByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "snapshot",
"name": "Image-Name",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.SnapshotByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.SnapshotByTag(ctx, "testing-1", "Image-Name")
if err != nil {
t.Errorf("DropletActions.SnapshotByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.SnapshotByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_EnableBackups(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "enable_backups",
}
mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.EnableBackups(ctx, 1)
if err != nil {
t.Errorf("DropletActions.EnableBackups returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.EnableBackups returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_EnableBackupsByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "enable_backups",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.EnableBackupByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.EnableBackupsByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.EnableBackupsByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.EnableBackupsByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_DisableBackups(t *testing.T) { func TestDropletAction_DisableBackups(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -351,7 +617,7 @@ func TestDropletAction_DisableBackups(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.DisableBackups(1) action, _, err := client.DropletActions.DisableBackups(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.DisableBackups returned error: %v", err) t.Errorf("DropletActions.DisableBackups returned error: %v", err)
} }
@ -362,6 +628,45 @@ func TestDropletAction_DisableBackups(t *testing.T) {
} }
} }
func TestDropletAction_DisableBackupsByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "disable_backups",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.DisableBackupsByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.DisableBackupsByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.DisableBackupsByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.DisableBackupsByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_PasswordReset(t *testing.T) { func TestDropletAction_PasswordReset(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -386,7 +691,7 @@ func TestDropletAction_PasswordReset(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.PasswordReset(1) action, _, err := client.DropletActions.PasswordReset(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.PasswordReset returned error: %v", err) t.Errorf("DropletActions.PasswordReset returned error: %v", err)
} }
@ -422,7 +727,7 @@ func TestDropletAction_RebuildByImageID(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.RebuildByImageID(1, 2) action, _, err := client.DropletActions.RebuildByImageID(ctx, 1, 2)
if err != nil { if err != nil {
t.Errorf("DropletActions.RebuildByImageID returned error: %v", err) t.Errorf("DropletActions.RebuildByImageID returned error: %v", err)
} }
@ -458,7 +763,7 @@ func TestDropletAction_RebuildByImageSlug(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.RebuildByImageSlug(1, "Image-Name") action, _, err := client.DropletActions.RebuildByImageSlug(ctx, 1, "Image-Name")
if err != nil { if err != nil {
t.Errorf("DropletActions.RebuildByImageSlug returned error: %v", err) t.Errorf("DropletActions.RebuildByImageSlug returned error: %v", err)
} }
@ -494,7 +799,7 @@ func TestDropletAction_ChangeKernel(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.ChangeKernel(1, 2) action, _, err := client.DropletActions.ChangeKernel(ctx, 1, 2)
if err != nil { if err != nil {
t.Errorf("DropletActions.ChangeKernel returned error: %v", err) t.Errorf("DropletActions.ChangeKernel returned error: %v", err)
} }
@ -529,7 +834,7 @@ func TestDropletAction_EnableIPv6(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.EnableIPv6(1) action, _, err := client.DropletActions.EnableIPv6(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.EnableIPv6 returned error: %v", err) t.Errorf("DropletActions.EnableIPv6 returned error: %v", err)
} }
@ -540,6 +845,45 @@ func TestDropletAction_EnableIPv6(t *testing.T) {
} }
} }
func TestDropletAction_EnableIPv6ByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "enable_ipv6",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.EnableIPv6ByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.EnableIPv6ByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.EnableIPv6ByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.EnableIPv6byTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_EnablePrivateNetworking(t *testing.T) { func TestDropletAction_EnablePrivateNetworking(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -564,7 +908,7 @@ func TestDropletAction_EnablePrivateNetworking(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.EnablePrivateNetworking(1) action, _, err := client.DropletActions.EnablePrivateNetworking(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.EnablePrivateNetworking returned error: %v", err) t.Errorf("DropletActions.EnablePrivateNetworking returned error: %v", err)
} }
@ -575,6 +919,45 @@ func TestDropletAction_EnablePrivateNetworking(t *testing.T) {
} }
} }
func TestDropletAction_EnablePrivateNetworkingByTag(t *testing.T) {
setup()
defer teardown()
request := &ActionRequest{
"type": "enable_private_networking",
}
mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("DropletActions.EnablePrivateNetworkingByTag did not request with a tag parameter")
}
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
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.EnablePrivateNetworkingByTag(ctx, "testing-1")
if err != nil {
t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned %+v, expected %+v", action, expected)
}
}
func TestDropletAction_Upgrade(t *testing.T) { func TestDropletAction_Upgrade(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -599,7 +982,7 @@ func TestDropletAction_Upgrade(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.Upgrade(1) action, _, err := client.DropletActions.Upgrade(ctx, 1)
if err != nil { if err != nil {
t.Errorf("DropletActions.Upgrade returned error: %v", err) t.Errorf("DropletActions.Upgrade returned error: %v", err)
} }
@ -619,7 +1002,7 @@ func TestDropletActions_Get(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.DropletActions.Get(123, 456) action, _, err := client.DropletActions.Get(ctx, 123, 456)
if err != nil { if err != nil {
t.Errorf("DropletActions.Get returned error: %v", err) t.Errorf("DropletActions.Get returned error: %v", err)
} }

View File

@ -1,28 +1,35 @@
package godo package godo
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
) )
const dropletBasePath = "v2/droplets" const dropletBasePath = "v2/droplets"
// DropletsService is an interface for interfacing with the droplet var errNoNetworks = errors.New("no networks have been defined")
// DropletsService is an interface for interfacing with the Droplet
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#droplets // See: https://developers.digitalocean.com/documentation/v2#droplets
type DropletsService interface { type DropletsService interface {
List(*ListOptions) ([]Droplet, *Response, error) List(context.Context, *ListOptions) ([]Droplet, *Response, error)
Get(int) (*Droplet, *Response, error) ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error)
Create(*DropletCreateRequest) (*Droplet, *Response, error) Get(context.Context, int) (*Droplet, *Response, error)
Delete(int) (*Response, error) Create(context.Context, *DropletCreateRequest) (*Droplet, *Response, error)
Kernels(int, *ListOptions) ([]Kernel, *Response, error) CreateMultiple(context.Context, *DropletMultiCreateRequest) ([]Droplet, *Response, error)
Snapshots(int, *ListOptions) ([]Image, *Response, error) Delete(context.Context, int) (*Response, error)
Backups(int, *ListOptions) ([]Image, *Response, error) DeleteByTag(context.Context, string) (*Response, error)
Actions(int, *ListOptions) ([]Action, *Response, error) Kernels(context.Context, int, *ListOptions) ([]Kernel, *Response, error)
Neighbors(int) ([]Droplet, *Response, error) Snapshots(context.Context, int, *ListOptions) ([]Image, *Response, error)
Backups(context.Context, int, *ListOptions) ([]Image, *Response, error)
Actions(context.Context, int, *ListOptions) ([]Action, *Response, error)
Neighbors(context.Context, int) ([]Droplet, *Response, error)
} }
// DropletsServiceOp handles communication with the droplet related methods of the // DropletsServiceOp handles communication with the Droplet related methods of the
// DigitalOcean API. // DigitalOcean API.
type DropletsServiceOp struct { type DropletsServiceOp struct {
client *Client client *Client
@ -42,12 +49,61 @@ type Droplet struct {
Size *Size `json:"size,omitempty"` Size *Size `json:"size,omitempty"`
SizeSlug string `json:"size_slug,omitempty"` SizeSlug string `json:"size_slug,omitempty"`
BackupIDs []int `json:"backup_ids,omitempty"` BackupIDs []int `json:"backup_ids,omitempty"`
NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"`
SnapshotIDs []int `json:"snapshot_ids,omitempty"` SnapshotIDs []int `json:"snapshot_ids,omitempty"`
Features []string `json:"features,omitempty"`
Locked bool `json:"locked,bool,omitempty"` Locked bool `json:"locked,bool,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Networks *Networks `json:"networks,omitempty"` Networks *Networks `json:"networks,omitempty"`
ActionIDs []int `json:"action_ids,omitempty"`
Created string `json:"created_at,omitempty"` Created string `json:"created_at,omitempty"`
Kernel *Kernel `json:"kernel,omitempty"`
Tags []string `json:"tags,omitempty"`
VolumeIDs []string `json:"volume_ids"`
}
// PublicIPv4 returns the public IPv4 address for the Droplet.
func (d *Droplet) PublicIPv4() (string, error) {
if d.Networks == nil {
return "", errNoNetworks
}
for _, v4 := range d.Networks.V4 {
if v4.Type == "public" {
return v4.IPAddress, nil
}
}
return "", nil
}
// PrivateIPv4 returns the private IPv4 address for the Droplet.
func (d *Droplet) PrivateIPv4() (string, error) {
if d.Networks == nil {
return "", errNoNetworks
}
for _, v4 := range d.Networks.V4 {
if v4.Type == "private" {
return v4.IPAddress, nil
}
}
return "", nil
}
// PublicIPv6 returns the public IPv6 address for the Droplet.
func (d *Droplet) PublicIPv6() (string, error) {
if d.Networks == nil {
return "", errNoNetworks
}
for _, v6 := range d.Networks.V6 {
if v6.Type == "public" {
return v6.IPAddress, nil
}
}
return "", nil
} }
// Kernel object // Kernel object
@ -57,6 +113,12 @@ type Kernel struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
} }
// BackupWindow object
type BackupWindow struct {
Start *Timestamp `json:"start,omitempty"`
End *Timestamp `json:"end,omitempty"`
}
// Convert Droplet to a string // Convert Droplet to a string
func (d Droplet) String() string { func (d Droplet) String() string {
return Stringify(d) return Stringify(d)
@ -78,7 +140,7 @@ type kernelsRoot struct {
Links *Links `json:"links"` Links *Links `json:"links"`
} }
type snapshotsRoot struct { type dropletSnapshotsRoot struct {
Snapshots []Image `json:"snapshots,omitempty"` Snapshots []Image `json:"snapshots,omitempty"`
Links *Links `json:"links"` Links *Links `json:"links"`
} }
@ -104,6 +166,27 @@ func (d DropletCreateImage) MarshalJSON() ([]byte, error) {
return json.Marshal(d.ID) return json.Marshal(d.ID)
} }
// DropletCreateVolume identifies a volume to attach for the create request. It
// prefers Name over ID,
type DropletCreateVolume struct {
ID string
Name string
}
// MarshalJSON returns an object with either the name or id of the volume. It
// returns the id if the name is empty.
func (d DropletCreateVolume) MarshalJSON() ([]byte, error) {
if d.Name != "" {
return json.Marshal(struct {
Name string `json:"name"`
}{Name: d.Name})
}
return json.Marshal(struct {
ID string `json:"id"`
}{ID: d.ID})
}
// DropletCreateSSHKey identifies a SSH Key for the create request. It prefers fingerprint over ID. // DropletCreateSSHKey identifies a SSH Key for the create request. It prefers fingerprint over ID.
type DropletCreateSSHKey struct { type DropletCreateSSHKey struct {
ID int ID int
@ -120,7 +203,7 @@ func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) {
return json.Marshal(d.ID) return json.Marshal(d.ID)
} }
// DropletCreateRequest represents a request to create a droplet. // DropletCreateRequest represents a request to create a Droplet.
type DropletCreateRequest struct { type DropletCreateRequest struct {
Name string `json:"name"` Name string `json:"name"`
Region string `json:"region"` Region string `json:"region"`
@ -130,20 +213,42 @@ type DropletCreateRequest struct {
Backups bool `json:"backups"` Backups bool `json:"backups"`
IPv6 bool `json:"ipv6"` IPv6 bool `json:"ipv6"`
PrivateNetworking bool `json:"private_networking"` PrivateNetworking bool `json:"private_networking"`
Monitoring bool `json:"monitoring"`
UserData string `json:"user_data,omitempty"` UserData string `json:"user_data,omitempty"`
Volumes []DropletCreateVolume `json:"volumes,omitempty"`
Tags []string `json:"tags"`
}
// DropletMultiCreateRequest is a request to create multiple Droplets.
type DropletMultiCreateRequest struct {
Names []string `json:"names"`
Region string `json:"region"`
Size string `json:"size"`
Image DropletCreateImage `json:"image"`
SSHKeys []DropletCreateSSHKey `json:"ssh_keys"`
Backups bool `json:"backups"`
IPv6 bool `json:"ipv6"`
PrivateNetworking bool `json:"private_networking"`
Monitoring bool `json:"monitoring"`
UserData string `json:"user_data,omitempty"`
Tags []string `json:"tags"`
} }
func (d DropletCreateRequest) String() string { func (d DropletCreateRequest) String() string {
return Stringify(d) return Stringify(d)
} }
// Networks represents the droplet's networks func (d DropletMultiCreateRequest) String() string {
return Stringify(d)
}
// Networks represents the Droplet's Networks.
type Networks struct { type Networks struct {
V4 []NetworkV4 `json:"v4,omitempty"` V4 []NetworkV4 `json:"v4,omitempty"`
V6 []NetworkV6 `json:"v6,omitempty"` V6 []NetworkV6 `json:"v6,omitempty"`
} }
// NetworkV4 represents a DigitalOcean IPv4 Network // NetworkV4 represents a DigitalOcean IPv4 Network.
type NetworkV4 struct { type NetworkV4 struct {
IPAddress string `json:"ip_address,omitempty"` IPAddress string `json:"ip_address,omitempty"`
Netmask string `json:"netmask,omitempty"` Netmask string `json:"netmask,omitempty"`
@ -167,15 +272,9 @@ func (n NetworkV6) String() string {
return Stringify(n) return Stringify(n)
} }
// List all droplets // Performs a list request given a path.
func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) { func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) {
path := dropletBasePath req, err := s.client.NewRequest(ctx, "GET", path, nil)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -192,15 +291,37 @@ func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error)
return root.Droplets, resp, err return root.Droplets, resp, err
} }
// Get individual droplet // List all Droplets.
func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) { func (s *DropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Droplet, *Response, error) {
path := dropletBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(ctx, path)
}
// ListByTag lists all Droplets matched by a Tag.
func (s *DropletsServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Droplet, *Response, error) {
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(ctx, path)
}
// Get individual Droplet.
func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID) path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -214,15 +335,15 @@ func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) {
return root.Droplet, resp, err return root.Droplet, resp, err
} }
// Create droplet // Create Droplet
func (s *DropletsServiceOp) Create(createRequest *DropletCreateRequest) (*Droplet, *Response, error) { func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCreateRequest) (*Droplet, *Response, error) {
if createRequest == nil { if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil") return nil, nil, NewArgError("createRequest", "cannot be nil")
} }
path := dropletBasePath path := dropletBasePath
req, err := s.client.NewRequest("POST", path, createRequest) req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -239,15 +360,34 @@ func (s *DropletsServiceOp) Create(createRequest *DropletCreateRequest) (*Drople
return root.Droplet, resp, err return root.Droplet, resp, err
} }
// Delete droplet // CreateMultiple creates multiple Droplets.
func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) { func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) {
if dropletID < 1 { if createRequest == nil {
return nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("createRequest", "cannot be nil")
} }
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID) path := dropletBasePath
req, err := s.client.NewRequest("DELETE", path, nil) req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(dropletsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Droplets, resp, err
}
// Performs a delete request given a path
func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) {
req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -257,8 +397,30 @@ func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) {
return resp, err return resp, err
} }
// Kernels lists kernels available for a droplet. // Delete Droplet.
func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel, *Response, error) { func (s *DropletsServiceOp) Delete(ctx context.Context, dropletID int) (*Response, error) {
if dropletID < 1 {
return nil, NewArgError("dropletID", "cannot be less than 1")
}
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
return s.delete(ctx, path)
}
// DeleteByTag deletes Droplets matched by a Tag.
func (s *DropletsServiceOp) DeleteByTag(ctx context.Context, tag string) (*Response, error) {
if tag == "" {
return nil, NewArgError("tag", "cannot be empty")
}
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
return s.delete(ctx, path)
}
// Kernels lists kernels available for a Droplet.
func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *ListOptions) ([]Kernel, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
@ -269,7 +431,7 @@ func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel,
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -283,8 +445,8 @@ func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel,
return root.Kernels, resp, err return root.Kernels, resp, err
} }
// Actions lists the actions for a droplet. // Actions lists the actions for a Droplet.
func (s *DropletsServiceOp) Actions(dropletID int, opt *ListOptions) ([]Action, *Response, error) { func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *ListOptions) ([]Action, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
@ -295,7 +457,7 @@ func (s *DropletsServiceOp) Actions(dropletID int, opt *ListOptions) ([]Action,
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -312,8 +474,8 @@ func (s *DropletsServiceOp) Actions(dropletID int, opt *ListOptions) ([]Action,
return root.Actions, resp, err return root.Actions, resp, err
} }
// Backups lists the backups for a droplet. // Backups lists the backups for a Droplet.
func (s *DropletsServiceOp) Backups(dropletID int, opt *ListOptions) ([]Image, *Response, error) { func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
@ -324,7 +486,7 @@ func (s *DropletsServiceOp) Backups(dropletID int, opt *ListOptions) ([]Image, *
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -341,8 +503,8 @@ func (s *DropletsServiceOp) Backups(dropletID int, opt *ListOptions) ([]Image, *
return root.Backups, resp, err return root.Backups, resp, err
} }
// Snapshots lists the snapshots available for a droplet. // Snapshots lists the snapshots available for a Droplet.
func (s *DropletsServiceOp) Snapshots(dropletID int, opt *ListOptions) ([]Image, *Response, error) { func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
@ -353,12 +515,12 @@ func (s *DropletsServiceOp) Snapshots(dropletID int, opt *ListOptions) ([]Image,
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
root := new(snapshotsRoot) root := new(dropletSnapshotsRoot)
resp, err := s.client.Do(req, root) resp, err := s.client.Do(req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
@ -370,15 +532,15 @@ func (s *DropletsServiceOp) Snapshots(dropletID int, opt *ListOptions) ([]Image,
return root.Snapshots, resp, err return root.Snapshots, resp, err
} }
// Neighbors lists the neighbors for a droplet. // Neighbors lists the neighbors for a Droplet.
func (s *DropletsServiceOp) Neighbors(dropletID int) ([]Droplet, *Response, error) { func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Droplet, *Response, error) {
if dropletID < 1 { if dropletID < 1 {
return nil, nil, NewArgError("dropletID", "cannot be less than 1") return nil, nil, NewArgError("dropletID", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID) path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -392,8 +554,8 @@ func (s *DropletsServiceOp) Neighbors(dropletID int) ([]Droplet, *Response, erro
return root.Droplets, resp, err return root.Droplets, resp, err
} }
func (s *DropletsServiceOp) dropletActionStatus(uri string) (string, error) { func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) {
action, _, err := s.client.DropletActions.GetByURI(uri) action, _, err := s.client.DropletActions.GetByURI(ctx, uri)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -17,14 +17,38 @@ func TestDroplets_ListDroplets(t *testing.T) {
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
}) })
droplets, _, err := client.Droplets.List(nil) droplets, _, err := client.Droplets.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Droplets.List returned error: %v", err) t.Errorf("Droplets.List returned error: %v", err)
} }
expected := []Droplet{{ID: 1}, {ID: 2}} expected := []Droplet{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(droplets, expected) { if !reflect.DeepEqual(droplets, expected) {
t.Errorf("Droplets.List returned %+v, expected %+v", droplets, expected) t.Errorf("Droplets.List\n got=%#v\nwant=%#v", droplets, expected)
}
}
func TestDroplets_ListDropletsByTag(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("Droplets.ListByTag did not request with a tag parameter")
}
testMethod(t, r, "GET")
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
})
droplets, _, err := client.Droplets.ListByTag(ctx, "testing-1", nil)
if err != nil {
t.Errorf("Droplets.ListByTag returned error: %v", err)
}
expected := []Droplet{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(droplets, expected) {
t.Errorf("Droplets.ListByTag returned %+v, expected %+v", droplets, expected)
} }
} }
@ -53,7 +77,7 @@ func TestDroplets_ListDropletsMultiplePages(t *testing.T) {
fmt.Fprint(w, string(b)) fmt.Fprint(w, string(b))
}) })
_, resp, err := client.Droplets.List(nil) _, resp, err := client.Droplets.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -84,7 +108,7 @@ func TestDroplets_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Droplets.List(opt) _, resp, err := client.Droplets.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -101,14 +125,14 @@ func TestDroplets_GetDroplet(t *testing.T) {
fmt.Fprint(w, `{"droplet":{"id":12345}}`) fmt.Fprint(w, `{"droplet":{"id":12345}}`)
}) })
droplets, _, err := client.Droplets.Get(12345) droplets, _, err := client.Droplets.Get(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Droplet.Get returned error: %v", err) t.Errorf("Droplet.Get returned error: %v", err)
} }
expected := &Droplet{ID: 12345} expected := &Droplet{ID: 12345}
if !reflect.DeepEqual(droplets, expected) { if !reflect.DeepEqual(droplets, expected) {
t.Errorf("Droplets.Get returned %+v, expected %+v", droplets, expected) t.Errorf("Droplets.Get\n got=%#v\nwant=%#v", droplets, expected)
} }
} }
@ -123,6 +147,12 @@ func TestDroplets_Create(t *testing.T) {
Image: DropletCreateImage{ Image: DropletCreateImage{
ID: 1, ID: 1,
}, },
Volumes: []DropletCreateVolume{
{Name: "hello-im-a-volume"},
{ID: "hello-im-another-volume"},
{Name: "hello-im-still-a-volume", ID: "should be ignored due to Name"},
},
Tags: []string{"one", "two"},
} }
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
@ -135,6 +165,68 @@ func TestDroplets_Create(t *testing.T) {
"backups": false, "backups": false,
"ipv6": false, "ipv6": false,
"private_networking": false, "private_networking": false,
"monitoring": false,
"volumes": []interface{}{
map[string]interface{}{"name": "hello-im-a-volume"},
map[string]interface{}{"id": "hello-im-another-volume"},
map[string]interface{}{"name": "hello-im-still-a-volume"},
},
"tags": []interface{}{"one", "two"},
}
var v map[string]interface{}
err := json.NewDecoder(r.Body).Decode(&v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
if !reflect.DeepEqual(v, expected) {
t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected)
}
fmt.Fprintf(w, `{"droplet":{"id":1}, "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "create"}]}}`)
})
droplet, resp, err := client.Droplets.Create(ctx, createRequest)
if err != nil {
t.Errorf("Droplets.Create returned error: %v", err)
}
if id := droplet.ID; id != 1 {
t.Errorf("expected id '%d', received '%d'", 1, id)
}
if a := resp.Links.Actions[0]; a.ID != 1 {
t.Errorf("expected action id '%d', received '%d'", 1, a.ID)
}
}
func TestDroplets_CreateMultiple(t *testing.T) {
setup()
defer teardown()
createRequest := &DropletMultiCreateRequest{
Names: []string{"name1", "name2"},
Region: "region",
Size: "size",
Image: DropletCreateImage{
ID: 1,
},
Tags: []string{"one", "two"},
}
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
expected := map[string]interface{}{
"names": []interface{}{"name1", "name2"},
"region": "region",
"size": "size",
"image": float64(1),
"ssh_keys": nil,
"backups": false,
"ipv6": false,
"private_networking": false,
"monitoring": false,
"tags": []interface{}{"one", "two"},
} }
var v map[string]interface{} var v map[string]interface{}
@ -147,15 +239,19 @@ func TestDroplets_Create(t *testing.T) {
t.Errorf("Request body = %#v, expected %#v", v, expected) t.Errorf("Request body = %#v, expected %#v", v, expected)
} }
fmt.Fprintf(w, `{"droplet":{"id":1}, "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "create"}]}}`) fmt.Fprintf(w, `{"droplets":[{"id":1},{"id":2}], "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "multiple_create"}]}}`)
}) })
droplet, resp, err := client.Droplets.Create(createRequest) droplets, resp, err := client.Droplets.CreateMultiple(ctx, createRequest)
if err != nil { if err != nil {
t.Errorf("Droplets.Create returned error: %v", err) t.Errorf("Droplets.CreateMultiple returned error: %v", err)
} }
if id := droplet.ID; id != 1 { if id := droplets[0].ID; id != 1 {
t.Errorf("expected id '%d', received '%d'", 1, id)
}
if id := droplets[1].ID; id != 2 {
t.Errorf("expected id '%d', received '%d'", 1, id) t.Errorf("expected id '%d', received '%d'", 1, id)
} }
@ -172,7 +268,25 @@ func TestDroplets_Destroy(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Droplets.Delete(12345) _, err := client.Droplets.Delete(ctx, 12345)
if err != nil {
t.Errorf("Droplet.Delete returned error: %v", err)
}
}
func TestDroplets_DestroyByTag(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("tag_name") != "testing-1" {
t.Errorf("Droplets.DeleteByTag did not request with a tag parameter")
}
testMethod(t, r, "DELETE")
})
_, err := client.Droplets.DeleteByTag(ctx, "testing-1")
if err != nil { if err != nil {
t.Errorf("Droplet.Delete returned error: %v", err) t.Errorf("Droplet.Delete returned error: %v", err)
} }
@ -188,14 +302,14 @@ func TestDroplets_Kernels(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
kernels, _, err := client.Droplets.Kernels(12345, opt) kernels, _, err := client.Droplets.Kernels(ctx, 12345, opt)
if err != nil { if err != nil {
t.Errorf("Droplets.Kernels returned error: %v", err) t.Errorf("Droplets.Kernels returned error: %v", err)
} }
expected := []Kernel{{ID: 1}, {ID: 2}} expected := []Kernel{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(kernels, expected) { if !reflect.DeepEqual(kernels, expected) {
t.Errorf("Droplets.Kernels returned %+v, expected %+v", kernels, expected) t.Errorf("Droplets.Kernels\n got=%#v\nwant=%#v", kernels, expected)
} }
} }
@ -209,14 +323,14 @@ func TestDroplets_Snapshots(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
snapshots, _, err := client.Droplets.Snapshots(12345, opt) snapshots, _, err := client.Droplets.Snapshots(ctx, 12345, opt)
if err != nil { if err != nil {
t.Errorf("Droplets.Snapshots returned error: %v", err) t.Errorf("Droplets.Snapshots returned error: %v", err)
} }
expected := []Image{{ID: 1}, {ID: 2}} expected := []Image{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(snapshots, expected) { if !reflect.DeepEqual(snapshots, expected) {
t.Errorf("Droplets.Snapshots returned %+v, expected %+v", snapshots, expected) t.Errorf("Droplets.Snapshots\n got=%#v\nwant=%#v", snapshots, expected)
} }
} }
@ -230,14 +344,14 @@ func TestDroplets_Backups(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
backups, _, err := client.Droplets.Backups(12345, opt) backups, _, err := client.Droplets.Backups(ctx, 12345, opt)
if err != nil { if err != nil {
t.Errorf("Droplets.Backups returned error: %v", err) t.Errorf("Droplets.Backups returned error: %v", err)
} }
expected := []Image{{ID: 1}, {ID: 2}} expected := []Image{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(backups, expected) { if !reflect.DeepEqual(backups, expected) {
t.Errorf("Droplets.Backups returned %+v, expected %+v", backups, expected) t.Errorf("Droplets.Backups\n got=%#v\nwant=%#v", backups, expected)
} }
} }
@ -251,14 +365,14 @@ func TestDroplets_Actions(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
actions, _, err := client.Droplets.Actions(12345, opt) actions, _, err := client.Droplets.Actions(ctx, 12345, opt)
if err != nil { if err != nil {
t.Errorf("Droplets.Actions returned error: %v", err) t.Errorf("Droplets.Actions returned error: %v", err)
} }
expected := []Action{{ID: 1}, {ID: 2}} expected := []Action{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(actions, expected) { if !reflect.DeepEqual(actions, expected) {
t.Errorf("Droplets.Actions returned %+v, expected %+v", actions, expected) t.Errorf("Droplets.Actions\n got=%#v\nwant=%#v", actions, expected)
} }
} }
@ -271,14 +385,14 @@ func TestDroplets_Neighbors(t *testing.T) {
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
}) })
neighbors, _, err := client.Droplets.Neighbors(12345) neighbors, _, err := client.Droplets.Neighbors(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Droplets.Neighbors returned error: %v", err) t.Errorf("Droplets.Neighbors returned error: %v", err)
} }
expected := []Droplet{{ID: 1}, {ID: 2}} expected := []Droplet{{ID: 1}, {ID: 2}}
if !reflect.DeepEqual(neighbors, expected) { if !reflect.DeepEqual(neighbors, expected) {
t.Errorf("Droplets.Neighbors returned %+v, expected %+v", neighbors, expected) t.Errorf("Droplets.Neighbors\n got=%#v\nwant=%#v", neighbors, expected)
} }
} }
@ -292,7 +406,7 @@ func TestNetworkV4_String(t *testing.T) {
stringified := network.String() stringified := network.String()
expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}` expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}`
if expected != stringified { if expected != stringified {
t.Errorf("NetworkV4.String returned %+v, expected %+v", stringified, expected) t.Errorf("NetworkV4.String\n got=%#v\nwant=%#v", stringified, expected)
} }
} }
@ -306,67 +420,49 @@ func TestNetworkV6_String(t *testing.T) {
stringified := network.String() stringified := network.String()
expected := `godo.NetworkV6{IPAddress:"2604:A880:0800:0010:0000:0000:02DD:4001", Netmask:64, Gateway:"2604:A880:0800:0010:0000:0000:0000:0001", Type:""}` expected := `godo.NetworkV6{IPAddress:"2604:A880:0800:0010:0000:0000:02DD:4001", Netmask:64, Gateway:"2604:A880:0800:0010:0000:0000:0000:0001", Type:""}`
if expected != stringified { if expected != stringified {
t.Errorf("NetworkV6.String returned %+v, expected %+v", stringified, expected) t.Errorf("NetworkV6.String\n got=%#v\nwant=%#v", stringified, expected)
} }
} }
func TestDroplet_String(t *testing.T) { func TestDroplets_IPMethods(t *testing.T) {
var d Droplet
region := &Region{ ipv6 := "1000:1000:1000:1000:0000:0000:004D:B001"
Slug: "region",
Name: "Region", d.Networks = &Networks{
Sizes: []string{"1", "2"}, V4: []NetworkV4{
Available: true, {IPAddress: "192.168.0.1", Type: "public"},
{IPAddress: "10.0.0.1", Type: "private"},
},
V6: []NetworkV6{
{IPAddress: ipv6, Type: "public"},
},
} }
image := &Image{ ip, err := d.PublicIPv4()
ID: 1, if err != nil {
Name: "Image", t.Errorf("unknown error")
Type: "snapshot",
Distribution: "Ubuntu",
Slug: "image",
Public: true,
Regions: []string{"one", "two"},
MinDiskSize: 20,
Created: "2013-11-27T09:24:55Z",
} }
size := &Size{ if got, expected := ip, "192.168.0.1"; got != expected {
Slug: "size", t.Errorf("Droplet.PublicIPv4 returned %s; expected %s", got, expected)
PriceMonthly: 123,
PriceHourly: 456,
Regions: []string{"1", "2"},
}
network := &NetworkV4{
IPAddress: "192.168.1.2",
Netmask: "255.255.255.0",
Gateway: "192.168.1.1",
}
networks := &Networks{
V4: []NetworkV4{*network},
} }
droplet := &Droplet{ ip, err = d.PrivateIPv4()
ID: 1, if err != nil {
Name: "droplet", t.Errorf("unknown error")
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,
SizeSlug: "1gb",
} }
stringified := droplet.String() if got, expected := ip, "10.0.0.1"; got != expected {
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", Type:"snapshot", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"], MinDiskSize:20, Created:"2013-11-27T09:24:55Z"}, Size:godo.Size{Slug:"size", Memory:0, Vcpus:0, Disk:0, PriceMonthly:123, PriceHourly:456, Regions:["1" "2"], Available:false, Transfer:0}, SizeSlug:"1gb", BackupIDs:[1], SnapshotIDs:[1], Locked:false, Status:"active", Networks:godo.Networks{V4:[godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}]}, ActionIDs:[1], Created:""}` t.Errorf("Droplet.PrivateIPv4 returned %s; expected %s", got, expected)
if expected != stringified { }
t.Errorf("Droplet.String returned %+v, expected %+v", stringified, expected)
ip, err = d.PublicIPv6()
if err != nil {
t.Errorf("unknown error")
}
if got, expected := ip, ipv6; got != expected {
t.Errorf("Droplet.PublicIPv6 returned %s; expected %s", got, expected)
} }
} }

134
vendor/github.com/digitalocean/godo/floating_ips.go generated vendored Normal file
View File

@ -0,0 +1,134 @@
package godo
import (
"context"
"fmt"
)
const floatingBasePath = "v2/floating_ips"
// FloatingIPsService is an interface for interfacing with the floating IPs
// endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#floating-ips
type FloatingIPsService interface {
List(context.Context, *ListOptions) ([]FloatingIP, *Response, error)
Get(context.Context, string) (*FloatingIP, *Response, error)
Create(context.Context, *FloatingIPCreateRequest) (*FloatingIP, *Response, error)
Delete(context.Context, string) (*Response, error)
}
// FloatingIPsServiceOp handles communication with the floating IPs related methods of the
// DigitalOcean API.
type FloatingIPsServiceOp struct {
client *Client
}
var _ FloatingIPsService = &FloatingIPsServiceOp{}
// FloatingIP represents a Digital Ocean floating IP.
type FloatingIP struct {
Region *Region `json:"region"`
Droplet *Droplet `json:"droplet"`
IP string `json:"ip"`
}
func (f FloatingIP) String() string {
return Stringify(f)
}
type floatingIPsRoot struct {
FloatingIPs []FloatingIP `json:"floating_ips"`
Links *Links `json:"links"`
}
type floatingIPRoot struct {
FloatingIP *FloatingIP `json:"floating_ip"`
Links *Links `json:"links,omitempty"`
}
// FloatingIPCreateRequest represents a request to create a floating IP.
// If DropletID is not empty, the floating IP will be assigned to the
// droplet.
type FloatingIPCreateRequest struct {
Region string `json:"region"`
DropletID int `json:"droplet_id,omitempty"`
}
// List all floating IPs.
func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]FloatingIP, *Response, error) {
path := floatingBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := f.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(floatingIPsRoot)
resp, err := f.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.FloatingIPs, resp, err
}
// Get an individual floating IP.
func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) {
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
req, err := f.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(floatingIPRoot)
resp, err := f.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.FloatingIP, resp, err
}
// Create a floating IP. If the DropletID field of the request is not empty,
// the floating IP will also be assigned to the droplet.
func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) {
path := floatingBasePath
req, err := f.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(floatingIPRoot)
resp, err := f.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.FloatingIP, resp, err
}
// Delete a floating IP.
func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
req, err := f.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := f.client.Do(req, nil)
return resp, err
}

View File

@ -0,0 +1,108 @@
package godo
import (
"context"
"fmt"
)
// FloatingIPActionsService is an interface for interfacing with the
// floating IPs actions endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#floating-ips-action
type FloatingIPActionsService interface {
Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error)
Unassign(ctx context.Context, ip string) (*Action, *Response, error)
Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error)
List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error)
}
// FloatingIPActionsServiceOp handles communication with the floating IPs
// action related methods of the DigitalOcean API.
type FloatingIPActionsServiceOp struct {
client *Client
}
// Assign a floating IP to a droplet.
func (s *FloatingIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) {
request := &ActionRequest{
"type": "assign",
"droplet_id": dropletID,
}
return s.doAction(ctx, ip, request)
}
// Unassign a floating IP from the droplet it is currently assigned to.
func (s *FloatingIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) {
request := &ActionRequest{"type": "unassign"}
return s.doAction(ctx, ip, request)
}
// Get an action for a particular floating IP by id.
func (s *FloatingIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) {
path := fmt.Sprintf("%s/%d", floatingIPActionPath(ip), actionID)
return s.get(ctx, path)
}
// List the actions for a particular floating IP.
func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) {
path := floatingIPActionPath(ip)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(ctx, path)
}
func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
path := floatingIPActionPath(ip)
req, err := s.client.NewRequest(ctx, "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
}
func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
req, err := s.client.NewRequest(ctx, "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 (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
req, err := s.client.NewRequest(ctx, "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
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Actions, resp, err
}
func floatingIPActionPath(ip string) string {
return fmt.Sprintf("%s/%s/actions", floatingBasePath, ip)
}

View File

@ -0,0 +1,167 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestFloatingIPsActions_Assign(t *testing.T) {
setup()
defer teardown()
dropletID := 12345
assignRequest := &ActionRequest{
"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
"type": "assign",
}
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, assignRequest) {
t.Errorf("Request body = %#v, expected %#v", v, assignRequest)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
assign, _, err := client.FloatingIPActions.Assign(ctx, "192.168.0.1", 12345)
if err != nil {
t.Errorf("FloatingIPsActions.Assign returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(assign, expected) {
t.Errorf("FloatingIPsActions.Assign returned %+v, expected %+v", assign, expected)
}
}
func TestFloatingIPsActions_Unassign(t *testing.T) {
setup()
defer teardown()
unassignRequest := &ActionRequest{
"type": "unassign",
}
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, unassignRequest) {
t.Errorf("Request body = %+v, expected %+v", v, unassignRequest)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.FloatingIPActions.Unassign(ctx, "192.168.0.1")
if err != nil {
t.Errorf("FloatingIPsActions.Get returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("FloatingIPsActions.Get returned %+v, expected %+v", action, expected)
}
}
func TestFloatingIPsActions_Get(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions/456", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.FloatingIPActions.Get(ctx, "192.168.0.1", 456)
if err != nil {
t.Errorf("FloatingIPsActions.Get returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("FloatingIPsActions.Get returned %+v, expected %+v", action, expected)
}
}
func TestFloatingIPsActions_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"actions":[{"status":"in-progress"}]}`)
})
actions, _, err := client.FloatingIPActions.List(ctx, "192.168.0.1", nil)
if err != nil {
t.Errorf("FloatingIPsActions.List returned error: %v", err)
}
expected := []Action{{Status: "in-progress"}}
if !reflect.DeepEqual(actions, expected) {
t.Errorf("FloatingIPsActions.List returned %+v, expected %+v", actions, expected)
}
}
func TestFloatingIPsActions_ListMultiplePages(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"actions":[{"status":"in-progress"}], "links":{"pages":{"next":"http://example.com/v2/floating_ips/192.168.0.1/actions?page=2"}}}`)
})
_, resp, err := client.FloatingIPActions.List(ctx, "192.168.0.1", nil)
if err != nil {
t.Errorf("FloatingIPsActions.List returned error: %v", err)
}
checkCurrentPage(t, resp, 1)
}
func TestFloatingIPsActions_ListPageByNumber(t *testing.T) {
setup()
defer teardown()
jBlob := `
{
"actions":[{"status":"in-progress"}],
"links":{
"pages":{
"next":"http://example.com/v2/regions/?page=3",
"prev":"http://example.com/v2/regions/?page=1",
"last":"http://example.com/v2/regions/?page=3",
"first":"http://example.com/v2/regions/?page=1"
}
}
}`
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
opt := &ListOptions{Page: 2}
_, resp, err := client.FloatingIPActions.List(ctx, "192.168.0.1", opt)
if err != nil {
t.Errorf("FloatingIPsActions.List returned error: %v", err)
}
checkCurrentPage(t, resp, 2)
}

View File

@ -0,0 +1,149 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestFloatingIPs_ListFloatingIPs(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}]}`)
})
floatingIPs, _, err := client.FloatingIPs.List(ctx, nil)
if err != nil {
t.Errorf("FloatingIPs.List returned error: %v", err)
}
expected := []FloatingIP{
{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"},
{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 2}, IP: "192.168.0.2"},
}
if !reflect.DeepEqual(floatingIPs, expected) {
t.Errorf("FloatingIPs.List returned %+v, expected %+v", floatingIPs, expected)
}
}
func TestFloatingIPs_ListFloatingIPsMultiplePages(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}], "links":{"pages":{"next":"http://example.com/v2/floating_ips/?page=2"}}}`)
})
_, resp, err := client.FloatingIPs.List(ctx, nil)
if err != nil {
t.Fatal(err)
}
checkCurrentPage(t, resp, 1)
}
func TestFloatingIPs_RetrievePageByNumber(t *testing.T) {
setup()
defer teardown()
jBlob := `
{
"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}],
"links":{
"pages":{
"next":"http://example.com/v2/floating_ips/?page=3",
"prev":"http://example.com/v2/floating_ips/?page=1",
"last":"http://example.com/v2/floating_ips/?page=3",
"first":"http://example.com/v2/floating_ips/?page=1"
}
}
}`
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
opt := &ListOptions{Page: 2}
_, resp, err := client.FloatingIPs.List(ctx, opt)
if err != nil {
t.Fatal(err)
}
checkCurrentPage(t, resp, 2)
}
func TestFloatingIPs_Get(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips/192.168.0.1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`)
})
floatingIP, _, err := client.FloatingIPs.Get(ctx, "192.168.0.1")
if err != nil {
t.Errorf("domain.Get returned error: %v", err)
}
expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"}
if !reflect.DeepEqual(floatingIP, expected) {
t.Errorf("FloatingIPs.Get returned %+v, expected %+v", floatingIP, expected)
}
}
func TestFloatingIPs_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &FloatingIPCreateRequest{
Region: "nyc3",
DropletID: 1,
}
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
v := new(FloatingIPCreateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprint(w, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`)
})
floatingIP, _, err := client.FloatingIPs.Create(ctx, createRequest)
if err != nil {
t.Errorf("FloatingIPs.Create returned error: %v", err)
}
expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"}
if !reflect.DeepEqual(floatingIP, expected) {
t.Errorf("FloatingIPs.Create returned %+v, expected %+v", floatingIP, expected)
}
}
func TestFloatingIPs_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/floating_ips/192.168.0.1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.FloatingIPs.Delete(ctx, "192.168.0.1")
if err != nil {
t.Errorf("FloatingIPs.Delete returned error: %v", err)
}
}

View File

@ -2,6 +2,7 @@ package godo
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -17,14 +18,14 @@ import (
) )
const ( const (
libraryVersion = "0.1.0" libraryVersion = "1.0.0"
defaultBaseURL = "https://api.digitalocean.com/" defaultBaseURL = "https://api.digitalocean.com/"
userAgent = "godo/" + libraryVersion userAgent = "godo/" + libraryVersion
mediaType = "application/json" mediaType = "application/json"
headerRateLimit = "X-RateLimit-Limit" headerRateLimit = "RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining" headerRateRemaining = "RateLimit-Remaining"
headerRateReset = "X-RateLimit-Reset" headerRateReset = "RateLimit-Reset"
) )
// Client manages communication with DigitalOcean V2 API. // Client manages communication with DigitalOcean V2 API.
@ -53,6 +54,14 @@ type Client struct {
Keys KeysService Keys KeysService
Regions RegionsService Regions RegionsService
Sizes SizesService Sizes SizesService
FloatingIPs FloatingIPsService
FloatingIPActions FloatingIPActionsService
Snapshots SnapshotsService
Storage StorageService
StorageActions StorageActionsService
Tags TagsService
LoadBalancers LoadBalancersService
Certificates CertificatesService
// Optional function called after every successful request made to the DO APIs // Optional function called after every successful request made to the DO APIs
onRequestCompleted RequestCompletionCallback onRequestCompleted RequestCompletionCallback
@ -91,7 +100,10 @@ type ErrorResponse struct {
Response *http.Response Response *http.Response
// Error message // Error message
Message string Message string `json:"message"`
// RequestID returned from the API, useful to contact support.
RequestID string `json:"request_id"`
} }
// Rate contains the rate limit for the current client. // Rate contains the rate limit for the current client.
@ -102,7 +114,7 @@ type Rate struct {
// The number of remaining requests the client can make this hour. // The number of remaining requests the client can make this hour.
Remaining int `json:"remaining"` Remaining int `json:"remaining"`
// The time at w\hic the current rate limit will reset. // The time at which the current rate limit will reset.
Reset Timestamp `json:"reset"` Reset Timestamp `json:"reset"`
} }
@ -147,19 +159,63 @@ func NewClient(httpClient *http.Client) *Client {
c.Domains = &DomainsServiceOp{client: c} c.Domains = &DomainsServiceOp{client: c}
c.Droplets = &DropletsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c}
c.DropletActions = &DropletActionsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c} c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c} c.ImageActions = &ImageActionsServiceOp{client: c}
c.Keys = &KeysServiceOp{client: c} c.Keys = &KeysServiceOp{client: c}
c.Regions = &RegionsServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c}
c.Snapshots = &SnapshotsServiceOp{client: c}
c.Sizes = &SizesServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c}
c.Storage = &StorageServiceOp{client: c}
c.StorageActions = &StorageActionsServiceOp{client: c}
c.Tags = &TagsServiceOp{client: c}
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
c.Certificates = &CertificatesServiceOp{client: c}
return c return c
} }
// ClientOpt are options for New.
type ClientOpt func(*Client) error
// New returns a new DIgitalOcean API client instance.
func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) {
c := NewClient(httpClient)
for _, opt := range opts {
if err := opt(c); err != nil {
return nil, err
}
}
return c, nil
}
// SetBaseURL is a client option for setting the base URL.
func SetBaseURL(bu string) ClientOpt {
return func(c *Client) error {
u, err := url.Parse(bu)
if err != nil {
return err
}
c.BaseURL = u
return nil
}
}
// SetUserAgent is a client option for setting the user agent.
func SetUserAgent(ua string) ClientOpt {
return func(c *Client) error {
c.UserAgent = fmt.Sprintf("%s+%s", ua, c.UserAgent)
return nil
}
}
// NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the // 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 // 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. // 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) { func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr) rel, err := url.Parse(urlStr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -169,7 +225,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
if body != nil { if body != nil {
err := json.NewEncoder(buf).Encode(body) err = json.NewEncoder(buf).Encode(body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -180,9 +236,10 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
return nil, err return nil, err
} }
req = req.WithContext(ctx)
req.Header.Add("Content-Type", mediaType) req.Header.Add("Content-Type", mediaType)
req.Header.Add("Accept", mediaType) req.Header.Add("Accept", mediaType)
req.Header.Add("User-Agent", userAgent) req.Header.Add("User-Agent", c.UserAgent)
return req, nil return req, nil
} }
@ -261,12 +318,12 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
if v != nil { if v != nil {
if w, ok := v.(io.Writer); ok { if w, ok := v.(io.Writer); ok {
_, err := io.Copy(w, resp.Body) _, err = io.Copy(w, resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
err := json.NewDecoder(resp.Body).Decode(v) err = json.NewDecoder(resp.Body).Decode(v)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -276,6 +333,10 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
return response, err return response, err
} }
func (r *ErrorResponse) Error() string { func (r *ErrorResponse) Error() string {
if r.RequestID != "" {
return fmt.Sprintf("%v %v: %d (request %q) %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
}
return fmt.Sprintf("%v %v: %d %v", return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message) r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
} }

View File

@ -1,7 +1,7 @@
package godo package godo
import ( import (
"encoding/json" "context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -17,6 +17,8 @@ import (
var ( var (
mux *http.ServeMux mux *http.ServeMux
ctx = context.TODO()
client *Client client *Client
server *httptest.Server server *httptest.Server
@ -68,17 +70,65 @@ func testURLParseError(t *testing.T, err error) {
} }
} }
func TestNewClient(t *testing.T) { func testClientServices(t *testing.T, c *Client) {
c := NewClient(nil) services := []string{
if c.BaseURL.String() != defaultBaseURL { "Account",
t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL.String(), defaultBaseURL) "Actions",
"Domains",
"Droplets",
"DropletActions",
"Images",
"ImageActions",
"Keys",
"Regions",
"Sizes",
"FloatingIPs",
"FloatingIPActions",
"Tags",
} }
cp := reflect.ValueOf(c)
cv := reflect.Indirect(cp)
for _, s := range services {
if cv.FieldByName(s).IsNil() {
t.Errorf("c.%s shouldn't be nil", s)
}
}
}
func testClientDefaultBaseURL(t *testing.T, c *Client) {
if c.BaseURL == nil || c.BaseURL.String() != defaultBaseURL {
t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL, defaultBaseURL)
}
}
func testClientDefaultUserAgent(t *testing.T, c *Client) {
if c.UserAgent != userAgent { if c.UserAgent != userAgent {
t.Errorf("NewClick UserAgent = %v, expected %v", c.UserAgent, userAgent) t.Errorf("NewClick UserAgent = %v, expected %v", c.UserAgent, userAgent)
} }
} }
func testClientDefaults(t *testing.T, c *Client) {
testClientDefaultBaseURL(t, c)
testClientDefaultUserAgent(t, c)
testClientServices(t, c)
}
func TestNewClient(t *testing.T) {
c := NewClient(nil)
testClientDefaults(t, c)
}
func TestNew(t *testing.T) {
c, err := New(nil)
if err != nil {
t.Fatalf("New(): %v", err)
}
testClientDefaults(t, c)
}
func TestNewRequest(t *testing.T) { func TestNewRequest(t *testing.T) {
c := NewClient(nil) c := NewClient(nil)
@ -86,8 +136,8 @@ func TestNewRequest(t *testing.T) {
inBody, outBody := &DropletCreateRequest{Name: "l"}, inBody, outBody := &DropletCreateRequest{Name: "l"},
`{"name":"l","region":"","size":"","image":0,`+ `{"name":"l","region":"","size":"","image":0,`+
`"ssh_keys":null,"backups":false,"ipv6":false,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+
`"private_networking":false}`+"\n" `"private_networking":false,"monitoring":false,"tags":null}`+"\n"
req, _ := c.NewRequest("GET", inURL, inBody) req, _ := c.NewRequest(ctx, "GET", inURL, inBody)
// test relative URL was expanded // test relative URL was expanded
if req.URL.String() != outURL { if req.URL.String() != outURL {
@ -114,8 +164,8 @@ func TestNewRequest_withUserData(t *testing.T) {
inBody, outBody := &DropletCreateRequest{Name: "l", UserData: "u"}, inBody, outBody := &DropletCreateRequest{Name: "l", UserData: "u"},
`{"name":"l","region":"","size":"","image":0,`+ `{"name":"l","region":"","size":"","image":0,`+
`"ssh_keys":null,"backups":false,"ipv6":false,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+
`"private_networking":false,"user_data":"u"}`+"\n" `"private_networking":false,"monitoring":false,"user_data":"u","tags":null}`+"\n"
req, _ := c.NewRequest("GET", inURL, inBody) req, _ := c.NewRequest(ctx, "GET", inURL, inBody)
// test relative URL was expanded // test relative URL was expanded
if req.URL.String() != outURL { if req.URL.String() != outURL {
@ -135,28 +185,28 @@ func TestNewRequest_withUserData(t *testing.T) {
} }
} }
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) { func TestNewRequest_badURL(t *testing.T) {
c := NewClient(nil) c := NewClient(nil)
_, err := c.NewRequest("GET", ":", nil) _, err := c.NewRequest(ctx, "GET", ":", nil)
testURLParseError(t, err) testURLParseError(t, err)
} }
func TestNewRequest_withCustomUserAgent(t *testing.T) {
ua := "testing"
c, err := New(nil, SetUserAgent(ua))
if err != nil {
t.Fatalf("New() unexpected error: %v", err)
}
req, _ := c.NewRequest(ctx, "GET", "/foo", nil)
expected := fmt.Sprintf("%s+%s", ua, userAgent)
if got := req.Header.Get("User-Agent"); got != expected {
t.Errorf("New() UserAgent = %s; expected %s", got, expected)
}
}
func TestDo(t *testing.T) { func TestDo(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@ -172,7 +222,7 @@ func TestDo(t *testing.T) {
fmt.Fprint(w, `{"A":"a"}`) fmt.Fprint(w, `{"A":"a"}`)
}) })
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
body := new(foo) body := new(foo)
_, err := client.Do(req, body) _, err := client.Do(req, body)
if err != nil { if err != nil {
@ -193,7 +243,7 @@ func TestDo_httpError(t *testing.T) {
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
}) })
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
_, err := client.Do(req, nil) _, err := client.Do(req, nil)
if err == nil { if err == nil {
@ -211,7 +261,7 @@ func TestDo_redirectLoop(t *testing.T) {
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusFound)
}) })
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
_, err := client.Do(req, nil) _, err := client.Do(req, nil)
if err == nil { if err == nil {
@ -296,7 +346,7 @@ func TestDo_rateLimit(t *testing.T) {
t.Errorf("Client rate reset not initialized to zero value") t.Errorf("Client rate reset not initialized to zero value")
} }
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
_, err := client.Do(req, nil) _, err := client.Do(req, nil)
if err != nil { if err != nil {
t.Fatalf("Do(): %v", err) t.Fatalf("Do(): %v", err)
@ -327,7 +377,7 @@ func TestDo_rateLimit_errorResponse(t *testing.T) {
var expected int var expected int
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
_, _ = client.Do(req, nil) _, _ = client.Do(req, nil)
if expected = 60; client.Rate.Limit != expected { if expected = 60; client.Rate.Limit != expected {
@ -369,7 +419,7 @@ func TestDo_completion_callback(t *testing.T) {
fmt.Fprint(w, `{"A":"a"}`) fmt.Fprint(w, `{"A":"a"}`)
}) })
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest(ctx, "GET", "/", nil)
body := new(foo) body := new(foo)
var completedReq *http.Request var completedReq *http.Request
var completedResp string var completedResp string
@ -453,3 +503,37 @@ func TestAddOptions(t *testing.T) {
} }
} }
} }
func TestCustomUserAgent(t *testing.T) {
c, err := New(nil, SetUserAgent("testing"))
if err != nil {
t.Fatalf("New() unexpected error: %v", err)
}
expected := fmt.Sprintf("%s+%s", "testing", userAgent)
if got := c.UserAgent; got != expected {
t.Errorf("New() UserAgent = %s; expected %s", got, expected)
}
}
func TestCustomBaseURL(t *testing.T) {
baseURL := "http://localhost/foo"
c, err := New(nil, SetBaseURL(baseURL))
if err != nil {
t.Fatalf("New() unexpected error: %v", err)
}
expected := baseURL
if got := c.BaseURL.String(); got != expected {
t.Errorf("New() BaseURL = %s; expected %s", got, expected)
}
}
func TestCustomBaseURL_badURL(t *testing.T) {
baseURL := ":"
_, err := New(nil, SetBaseURL(baseURL))
testURLParseError(t, err)
}

View File

@ -1,13 +1,17 @@
package godo package godo
import "fmt" import (
"context"
"fmt"
)
// ImageActionsService is an interface for interfacing with the image actions // ImageActionsService is an interface for interfacing with the image actions
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#image-actions // See: https://developers.digitalocean.com/documentation/v2#image-actions
type ImageActionsService interface { type ImageActionsService interface {
Get(int, int) (*Action, *Response, error) Get(context.Context, int, int) (*Action, *Response, error)
Transfer(int, *ActionRequest) (*Action, *Response, error) Transfer(context.Context, int, *ActionRequest) (*Action, *Response, error)
Convert(context.Context, int) (*Action, *Response, error)
} }
// ImageActionsServiceOp handles communition with the image action related methods of the // ImageActionsServiceOp handles communition with the image action related methods of the
@ -19,7 +23,7 @@ type ImageActionsServiceOp struct {
var _ ImageActionsService = &ImageActionsServiceOp{} var _ ImageActionsService = &ImageActionsServiceOp{}
// Transfer an image // Transfer an image
func (i *ImageActionsServiceOp) Transfer(imageID int, transferRequest *ActionRequest) (*Action, *Response, error) { func (i *ImageActionsServiceOp) Transfer(ctx context.Context, imageID int, transferRequest *ActionRequest) (*Action, *Response, error) {
if imageID < 1 { if imageID < 1 {
return nil, nil, NewArgError("imageID", "cannot be less than 1") return nil, nil, NewArgError("imageID", "cannot be less than 1")
} }
@ -30,7 +34,7 @@ func (i *ImageActionsServiceOp) Transfer(imageID int, transferRequest *ActionReq
path := fmt.Sprintf("v2/images/%d/actions", imageID) path := fmt.Sprintf("v2/images/%d/actions", imageID)
req, err := i.client.NewRequest("POST", path, transferRequest) req, err := i.client.NewRequest(ctx, "POST", path, transferRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -41,11 +45,37 @@ func (i *ImageActionsServiceOp) Transfer(imageID int, transferRequest *ActionReq
return nil, resp, err return nil, resp, err
} }
return &root.Event, resp, err return root.Event, resp, err
}
// Convert an image to a snapshot
func (i *ImageActionsServiceOp) Convert(ctx context.Context, imageID int) (*Action, *Response, error) {
if imageID < 1 {
return nil, nil, NewArgError("imageID", "cannont be less than 1")
}
path := fmt.Sprintf("v2/images/%d/actions", imageID)
convertRequest := &ActionRequest{
"type": "convert",
}
req, err := i.client.NewRequest(ctx, "POST", path, convertRequest)
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. // Get an action for a particular image by id.
func (i *ImageActionsServiceOp) Get(imageID, actionID int) (*Action, *Response, error) { func (i *ImageActionsServiceOp) Get(ctx context.Context, imageID, actionID int) (*Action, *Response, error) {
if imageID < 1 { if imageID < 1 {
return nil, nil, NewArgError("imageID", "cannot be less than 1") return nil, nil, NewArgError("imageID", "cannot be less than 1")
} }
@ -56,7 +86,7 @@ func (i *ImageActionsServiceOp) Get(imageID, actionID int) (*Action, *Response,
path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID) path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
req, err := i.client.NewRequest("GET", path, nil) req, err := i.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -67,5 +97,5 @@ func (i *ImageActionsServiceOp) Get(imageID, actionID int) (*Action, *Response,
return nil, resp, err return nil, resp, err
} }
return &root.Event, resp, err return root.Event, resp, err
} }

View File

@ -30,7 +30,42 @@ func TestImageActions_Transfer(t *testing.T) {
}) })
transfer, _, err := client.ImageActions.Transfer(12345, transferRequest) transfer, _, err := client.ImageActions.Transfer(ctx, 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_Convert(t *testing.T) {
setup()
defer teardown()
convertRequest := &ActionRequest{
"type": "convert",
}
mux.HandleFunc("/v2/images/12345/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, convertRequest) {
t.Errorf("Request body = %+v, expected %+v", v, convertRequest)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
transfer, _, err := client.ImageActions.Convert(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("ImageActions.Transfer returned error: %v", err) t.Errorf("ImageActions.Transfer returned error: %v", err)
} }
@ -50,7 +85,7 @@ func TestImageActions_Get(t *testing.T) {
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
}) })
action, _, err := client.ImageActions.Get(123, 456) action, _, err := client.ImageActions.Get(ctx, 123, 456)
if err != nil { if err != nil {
t.Errorf("ImageActions.Get returned error: %v", err) t.Errorf("ImageActions.Get returned error: %v", err)
} }

View File

@ -1,6 +1,9 @@
package godo package godo
import "fmt" import (
"context"
"fmt"
)
const imageBasePath = "v2/images" const imageBasePath = "v2/images"
@ -8,14 +11,14 @@ const imageBasePath = "v2/images"
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#images // See: https://developers.digitalocean.com/documentation/v2#images
type ImagesService interface { type ImagesService interface {
List(*ListOptions) ([]Image, *Response, error) List(context.Context, *ListOptions) ([]Image, *Response, error)
ListDistribution(opt *ListOptions) ([]Image, *Response, error) ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
ListApplication(opt *ListOptions) ([]Image, *Response, error) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
ListUser(opt *ListOptions) ([]Image, *Response, error) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
GetByID(int) (*Image, *Response, error) GetByID(context.Context, int) (*Image, *Response, error)
GetBySlug(string) (*Image, *Response, error) GetBySlug(context.Context, string) (*Image, *Response, error)
Update(int, *ImageUpdateRequest) (*Image, *Response, error) Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error)
Delete(int) (*Response, error) Delete(context.Context, int) (*Response, error)
} }
// ImagesServiceOp handles communication with the image related methods of the // ImagesServiceOp handles communication with the image related methods of the
@ -45,7 +48,7 @@ type ImageUpdateRequest struct {
} }
type imageRoot struct { type imageRoot struct {
Image Image Image *Image
} }
type imagesRoot struct { type imagesRoot struct {
@ -63,48 +66,48 @@ func (i Image) String() string {
} }
// List lists all the images available. // List lists all the images available.
func (s *ImagesServiceOp) List(opt *ListOptions) ([]Image, *Response, error) { func (s *ImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
return s.list(opt, nil) return s.list(ctx, opt, nil)
} }
// ListDistribution lists all the distribution images. // ListDistribution lists all the distribution images.
func (s *ImagesServiceOp) ListDistribution(opt *ListOptions) ([]Image, *Response, error) { func (s *ImagesServiceOp) ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
listOpt := listImageOptions{Type: "distribution"} listOpt := listImageOptions{Type: "distribution"}
return s.list(opt, &listOpt) return s.list(ctx, opt, &listOpt)
} }
// ListApplication lists all the application images. // ListApplication lists all the application images.
func (s *ImagesServiceOp) ListApplication(opt *ListOptions) ([]Image, *Response, error) { func (s *ImagesServiceOp) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
listOpt := listImageOptions{Type: "application"} listOpt := listImageOptions{Type: "application"}
return s.list(opt, &listOpt) return s.list(ctx, opt, &listOpt)
} }
// ListUser lists all the user images. // ListUser lists all the user images.
func (s *ImagesServiceOp) ListUser(opt *ListOptions) ([]Image, *Response, error) { func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
listOpt := listImageOptions{Private: true} listOpt := listImageOptions{Private: true}
return s.list(opt, &listOpt) return s.list(ctx, opt, &listOpt)
} }
// GetByID retrieves an image by id. // GetByID retrieves an image by id.
func (s *ImagesServiceOp) GetByID(imageID int) (*Image, *Response, error) { func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) {
if imageID < 1 { if imageID < 1 {
return nil, nil, NewArgError("imageID", "cannot be less than 1") return nil, nil, NewArgError("imageID", "cannot be less than 1")
} }
return s.get(interface{}(imageID)) return s.get(ctx, interface{}(imageID))
} }
// GetBySlug retrieves an image by slug. // GetBySlug retrieves an image by slug.
func (s *ImagesServiceOp) GetBySlug(slug string) (*Image, *Response, error) { func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, *Response, error) {
if len(slug) < 1 { if len(slug) < 1 {
return nil, nil, NewArgError("slug", "cannot be blank") return nil, nil, NewArgError("slug", "cannot be blank")
} }
return s.get(interface{}(slug)) return s.get(ctx, interface{}(slug))
} }
// Update an image name. // Update an image name.
func (s *ImagesServiceOp) Update(imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) { func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) {
if imageID < 1 { if imageID < 1 {
return nil, nil, NewArgError("imageID", "cannot be less than 1") return nil, nil, NewArgError("imageID", "cannot be less than 1")
} }
@ -114,7 +117,7 @@ func (s *ImagesServiceOp) Update(imageID int, updateRequest *ImageUpdateRequest)
} }
path := fmt.Sprintf("%s/%d", imageBasePath, imageID) path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
req, err := s.client.NewRequest("PUT", path, updateRequest) req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -125,18 +128,18 @@ func (s *ImagesServiceOp) Update(imageID int, updateRequest *ImageUpdateRequest)
return nil, resp, err return nil, resp, err
} }
return &root.Image, resp, err return root.Image, resp, err
} }
// Delete an image. // Delete an image.
func (s *ImagesServiceOp) Delete(imageID int) (*Response, error) { func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, error) {
if imageID < 1 { if imageID < 1 {
return nil, NewArgError("imageID", "cannot be less than 1") return nil, NewArgError("imageID", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d", imageBasePath, imageID) path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
req, err := s.client.NewRequest("DELETE", path, nil) req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,10 +150,10 @@ func (s *ImagesServiceOp) Delete(imageID int) (*Response, error) {
} }
// Helper method for getting an individual image // Helper method for getting an individual image
func (s *ImagesServiceOp) get(ID interface{}) (*Image, *Response, error) { func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
path := fmt.Sprintf("%s/%v", imageBasePath, ID) path := fmt.Sprintf("%s/%v", imageBasePath, ID)
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -161,11 +164,11 @@ func (s *ImagesServiceOp) get(ID interface{}) (*Image, *Response, error) {
return nil, resp, err return nil, resp, err
} }
return &root.Image, resp, err return root.Image, resp, err
} }
// Helper method for listing images // Helper method for listing images
func (s *ImagesServiceOp) list(opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) { func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) {
path := imageBasePath path := imageBasePath
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
@ -176,7 +179,7 @@ func (s *ImagesServiceOp) list(opt *ListOptions, listOpt *listImageOptions) ([]I
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -17,7 +17,7 @@ func TestImages_List(t *testing.T) {
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
}) })
images, _, err := client.Images.List(nil) images, _, err := client.Images.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Images.List returned error: %v", err) t.Errorf("Images.List returned error: %v", err)
} }
@ -42,7 +42,7 @@ func TestImages_ListDistribution(t *testing.T) {
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
}) })
images, _, err := client.Images.ListDistribution(nil) images, _, err := client.Images.ListDistribution(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Images.ListDistribution returned error: %v", err) t.Errorf("Images.ListDistribution returned error: %v", err)
} }
@ -67,7 +67,7 @@ func TestImages_ListApplication(t *testing.T) {
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
}) })
images, _, err := client.Images.ListApplication(nil) images, _, err := client.Images.ListApplication(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Images.ListApplication returned error: %v", err) t.Errorf("Images.ListApplication returned error: %v", err)
} }
@ -93,7 +93,7 @@ func TestImages_ListUser(t *testing.T) {
fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`)
}) })
images, _, err := client.Images.ListUser(nil) images, _, err := client.Images.ListUser(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Images.ListUser returned error: %v", err) t.Errorf("Images.ListUser returned error: %v", err)
} }
@ -113,7 +113,7 @@ func TestImages_ListImagesMultiplePages(t *testing.T) {
fmt.Fprint(w, `{"images": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/images/?page=2"}}}`) fmt.Fprint(w, `{"images": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/images/?page=2"}}}`)
}) })
_, resp, err := client.Images.List(&ListOptions{Page: 2}) _, resp, err := client.Images.List(ctx, &ListOptions{Page: 2})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -143,7 +143,7 @@ func TestImages_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Images.List(opt) _, resp, err := client.Images.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -160,7 +160,7 @@ func TestImages_GetImageByID(t *testing.T) {
fmt.Fprint(w, `{"image":{"id":12345}}`) fmt.Fprint(w, `{"image":{"id":12345}}`)
}) })
images, _, err := client.Images.GetByID(12345) images, _, err := client.Images.GetByID(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Image.GetByID returned error: %v", err) t.Errorf("Image.GetByID returned error: %v", err)
} }
@ -180,7 +180,7 @@ func TestImages_GetImageBySlug(t *testing.T) {
fmt.Fprint(w, `{"image":{"id":12345}}`) fmt.Fprint(w, `{"image":{"id":12345}}`)
}) })
images, _, err := client.Images.GetBySlug("ubuntu") images, _, err := client.Images.GetBySlug(ctx, "ubuntu")
if err != nil { if err != nil {
t.Errorf("Image.GetBySlug returned error: %v", err) t.Errorf("Image.GetBySlug returned error: %v", err)
} }
@ -217,7 +217,7 @@ func TestImages_Update(t *testing.T) {
fmt.Fprintf(w, `{"image":{"id":1}}`) fmt.Fprintf(w, `{"image":{"id":1}}`)
}) })
image, _, err := client.Images.Update(12345, updateRequest) image, _, err := client.Images.Update(ctx, 12345, updateRequest)
if err != nil { if err != nil {
t.Errorf("Images.Update returned error: %v", err) t.Errorf("Images.Update returned error: %v", err)
} else { } else {
@ -235,7 +235,7 @@ func TestImages_Destroy(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Images.Delete(12345) _, err := client.Images.Delete(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Image.Delete returned error: %v", err) t.Errorf("Image.Delete returned error: %v", err)
} }

View File

@ -1,6 +1,9 @@
package godo package godo
import "fmt" import (
"context"
"fmt"
)
const keysBasePath = "v2/account/keys" const keysBasePath = "v2/account/keys"
@ -8,14 +11,14 @@ const keysBasePath = "v2/account/keys"
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#keys // See: https://developers.digitalocean.com/documentation/v2#keys
type KeysService interface { type KeysService interface {
List(*ListOptions) ([]Key, *Response, error) List(context.Context, *ListOptions) ([]Key, *Response, error)
GetByID(int) (*Key, *Response, error) GetByID(context.Context, int) (*Key, *Response, error)
GetByFingerprint(string) (*Key, *Response, error) GetByFingerprint(context.Context, string) (*Key, *Response, error)
Create(*KeyCreateRequest) (*Key, *Response, error) Create(context.Context, *KeyCreateRequest) (*Key, *Response, error)
UpdateByID(int, *KeyUpdateRequest) (*Key, *Response, error) UpdateByID(context.Context, int, *KeyUpdateRequest) (*Key, *Response, error)
UpdateByFingerprint(string, *KeyUpdateRequest) (*Key, *Response, error) UpdateByFingerprint(context.Context, string, *KeyUpdateRequest) (*Key, *Response, error)
DeleteByID(int) (*Response, error) DeleteByID(context.Context, int) (*Response, error)
DeleteByFingerprint(string) (*Response, error) DeleteByFingerprint(context.Context, string) (*Response, error)
} }
// KeysServiceOp handles communication with key related method of the // KeysServiceOp handles communication with key related method of the
@ -45,7 +48,7 @@ type keysRoot struct {
} }
type keyRoot struct { type keyRoot struct {
SSHKey Key `json:"ssh_key"` SSHKey *Key `json:"ssh_key"`
} }
func (s Key) String() string { func (s Key) String() string {
@ -59,14 +62,14 @@ type KeyCreateRequest struct {
} }
// List all keys // List all keys
func (s *KeysServiceOp) List(opt *ListOptions) ([]Key, *Response, error) { func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Response, error) {
path := keysBasePath path := keysBasePath
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -84,8 +87,8 @@ func (s *KeysServiceOp) List(opt *ListOptions) ([]Key, *Response, error) {
} }
// Performs a get given a path // Performs a get given a path
func (s *KeysServiceOp) get(path string) (*Key, *Response, error) { func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -96,36 +99,36 @@ func (s *KeysServiceOp) get(path string) (*Key, *Response, error) {
return nil, resp, err return nil, resp, err
} }
return &root.SSHKey, resp, err return root.SSHKey, resp, err
} }
// GetByID gets a Key by id // GetByID gets a Key by id
func (s *KeysServiceOp) GetByID(keyID int) (*Key, *Response, error) { func (s *KeysServiceOp) GetByID(ctx context.Context, keyID int) (*Key, *Response, error) {
if keyID < 1 { if keyID < 1 {
return nil, nil, NewArgError("keyID", "cannot be less than 1") return nil, nil, NewArgError("keyID", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d", keysBasePath, keyID) path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
return s.get(path) return s.get(ctx, path)
} }
// GetByFingerprint gets a Key by by fingerprint // GetByFingerprint gets a Key by by fingerprint
func (s *KeysServiceOp) GetByFingerprint(fingerprint string) (*Key, *Response, error) { func (s *KeysServiceOp) GetByFingerprint(ctx context.Context, fingerprint string) (*Key, *Response, error) {
if len(fingerprint) < 1 { if len(fingerprint) < 1 {
return nil, nil, NewArgError("fingerprint", "cannot not be empty") return nil, nil, NewArgError("fingerprint", "cannot not be empty")
} }
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint) path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
return s.get(path) return s.get(ctx, path)
} }
// Create a key using a KeyCreateRequest // Create a key using a KeyCreateRequest
func (s *KeysServiceOp) Create(createRequest *KeyCreateRequest) (*Key, *Response, error) { func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequest) (*Key, *Response, error) {
if createRequest == nil { if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil") return nil, nil, NewArgError("createRequest", "cannot be nil")
} }
req, err := s.client.NewRequest("POST", keysBasePath, createRequest) req, err := s.client.NewRequest(ctx, "POST", keysBasePath, createRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -136,11 +139,11 @@ func (s *KeysServiceOp) Create(createRequest *KeyCreateRequest) (*Key, *Response
return nil, resp, err return nil, resp, err
} }
return &root.SSHKey, resp, err return root.SSHKey, resp, err
} }
// UpdateByID updates a key name by ID. // UpdateByID updates a key name by ID.
func (s *KeysServiceOp) UpdateByID(keyID int, updateRequest *KeyUpdateRequest) (*Key, *Response, error) { func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
if keyID < 1 { if keyID < 1 {
return nil, nil, NewArgError("keyID", "cannot be less than 1") return nil, nil, NewArgError("keyID", "cannot be less than 1")
} }
@ -150,7 +153,7 @@ func (s *KeysServiceOp) UpdateByID(keyID int, updateRequest *KeyUpdateRequest) (
} }
path := fmt.Sprintf("%s/%d", keysBasePath, keyID) path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
req, err := s.client.NewRequest("PUT", path, updateRequest) req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -161,11 +164,11 @@ func (s *KeysServiceOp) UpdateByID(keyID int, updateRequest *KeyUpdateRequest) (
return nil, resp, err return nil, resp, err
} }
return &root.SSHKey, resp, err return root.SSHKey, resp, err
} }
// UpdateByFingerprint updates a key name by fingerprint. // UpdateByFingerprint updates a key name by fingerprint.
func (s *KeysServiceOp) UpdateByFingerprint(fingerprint string, updateRequest *KeyUpdateRequest) (*Key, *Response, error) { func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint string, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
if len(fingerprint) < 1 { if len(fingerprint) < 1 {
return nil, nil, NewArgError("fingerprint", "cannot be empty") return nil, nil, NewArgError("fingerprint", "cannot be empty")
} }
@ -175,7 +178,7 @@ func (s *KeysServiceOp) UpdateByFingerprint(fingerprint string, updateRequest *K
} }
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint) path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
req, err := s.client.NewRequest("PUT", path, updateRequest) req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -186,12 +189,12 @@ func (s *KeysServiceOp) UpdateByFingerprint(fingerprint string, updateRequest *K
return nil, resp, err return nil, resp, err
} }
return &root.SSHKey, resp, err return root.SSHKey, resp, err
} }
// Delete key using a path // Delete key using a path
func (s *KeysServiceOp) delete(path string) (*Response, error) { func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
req, err := s.client.NewRequest("DELETE", path, nil) req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -202,21 +205,21 @@ func (s *KeysServiceOp) delete(path string) (*Response, error) {
} }
// DeleteByID deletes a key by its id // DeleteByID deletes a key by its id
func (s *KeysServiceOp) DeleteByID(keyID int) (*Response, error) { func (s *KeysServiceOp) DeleteByID(ctx context.Context, keyID int) (*Response, error) {
if keyID < 1 { if keyID < 1 {
return nil, NewArgError("keyID", "cannot be less than 1") return nil, NewArgError("keyID", "cannot be less than 1")
} }
path := fmt.Sprintf("%s/%d", keysBasePath, keyID) path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
return s.delete(path) return s.delete(ctx, path)
} }
// DeleteByFingerprint deletes a key by its fingerprint // DeleteByFingerprint deletes a key by its fingerprint
func (s *KeysServiceOp) DeleteByFingerprint(fingerprint string) (*Response, error) { func (s *KeysServiceOp) DeleteByFingerprint(ctx context.Context, fingerprint string) (*Response, error) {
if len(fingerprint) < 1 { if len(fingerprint) < 1 {
return nil, NewArgError("fingerprint", "cannot be empty") return nil, NewArgError("fingerprint", "cannot be empty")
} }
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint) path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
return s.delete(path) return s.delete(ctx, path)
} }

View File

@ -17,7 +17,7 @@ func TestKeys_List(t *testing.T) {
fmt.Fprint(w, `{"ssh_keys":[{"id":1},{"id":2}]}`) fmt.Fprint(w, `{"ssh_keys":[{"id":1},{"id":2}]}`)
}) })
keys, _, err := client.Keys.List(nil) keys, _, err := client.Keys.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Keys.List returned error: %v", err) t.Errorf("Keys.List returned error: %v", err)
} }
@ -37,7 +37,7 @@ func TestKeys_ListKeysMultiplePages(t *testing.T) {
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/account/keys/?page=2"}}}`) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/account/keys/?page=2"}}}`)
}) })
_, resp, err := client.Keys.List(nil) _, resp, err := client.Keys.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -67,7 +67,7 @@ func TestKeys_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Keys.List(opt) _, resp, err := client.Keys.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -83,7 +83,7 @@ func TestKeys_GetByID(t *testing.T) {
fmt.Fprint(w, `{"ssh_key": {"id":12345}}`) fmt.Fprint(w, `{"ssh_key": {"id":12345}}`)
}) })
keys, _, err := client.Keys.GetByID(12345) keys, _, err := client.Keys.GetByID(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Keys.GetByID returned error: %v", err) t.Errorf("Keys.GetByID returned error: %v", err)
} }
@ -103,7 +103,7 @@ func TestKeys_GetByFingerprint(t *testing.T) {
fmt.Fprint(w, `{"ssh_key": {"fingerprint":"aa:bb:cc"}}`) fmt.Fprint(w, `{"ssh_key": {"fingerprint":"aa:bb:cc"}}`)
}) })
keys, _, err := client.Keys.GetByFingerprint("aa:bb:cc") keys, _, err := client.Keys.GetByFingerprint(ctx, "aa:bb:cc")
if err != nil { if err != nil {
t.Errorf("Keys.GetByFingerprint returned error: %v", err) t.Errorf("Keys.GetByFingerprint returned error: %v", err)
} }
@ -138,7 +138,7 @@ func TestKeys_Create(t *testing.T) {
fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) fmt.Fprintf(w, `{"ssh_key":{"id":1}}`)
}) })
key, _, err := client.Keys.Create(createRequest) key, _, err := client.Keys.Create(ctx, createRequest)
if err != nil { if err != nil {
t.Errorf("Keys.Create returned error: %v", err) t.Errorf("Keys.Create returned error: %v", err)
} }
@ -175,7 +175,7 @@ func TestKeys_UpdateByID(t *testing.T) {
fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) fmt.Fprintf(w, `{"ssh_key":{"id":1}}`)
}) })
key, _, err := client.Keys.UpdateByID(12345, updateRequest) key, _, err := client.Keys.UpdateByID(ctx, 12345, updateRequest)
if err != nil { if err != nil {
t.Errorf("Keys.Update returned error: %v", err) t.Errorf("Keys.Update returned error: %v", err)
} else { } else {
@ -211,7 +211,7 @@ func TestKeys_UpdateByFingerprint(t *testing.T) {
fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) fmt.Fprintf(w, `{"ssh_key":{"id":1}}`)
}) })
key, _, err := client.Keys.UpdateByFingerprint("3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", updateRequest) key, _, err := client.Keys.UpdateByFingerprint(ctx, "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", updateRequest)
if err != nil { if err != nil {
t.Errorf("Keys.Update returned error: %v", err) t.Errorf("Keys.Update returned error: %v", err)
} else { } else {
@ -229,7 +229,7 @@ func TestKeys_DestroyByID(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Keys.DeleteByID(12345) _, err := client.Keys.DeleteByID(ctx, 12345)
if err != nil { if err != nil {
t.Errorf("Keys.Delete returned error: %v", err) t.Errorf("Keys.Delete returned error: %v", err)
} }
@ -243,7 +243,7 @@ func TestKeys_DestroyByFingerprint(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Keys.DeleteByFingerprint("aa:bb:cc") _, err := client.Keys.DeleteByFingerprint(ctx, "aa:bb:cc")
if err != nil { if err != nil {
t.Errorf("Keys.Delete returned error: %v", err) t.Errorf("Keys.Delete returned error: %v", err)
} }

View File

@ -1,6 +1,7 @@
package godo package godo
import ( import (
"context"
"net/url" "net/url"
"strconv" "strconv"
) )
@ -58,11 +59,7 @@ func (l *Links) IsLastPage() bool {
} }
func (p *Pages) isLast() bool { func (p *Pages) isLast() bool {
if p.Last == "" { return p.Last == ""
return true
}
return false
} }
func pageForURL(urlText string) (int, error) { func pageForURL(urlText string) (int, error) {
@ -81,6 +78,6 @@ func pageForURL(urlText string) (int, error) {
} }
// Get a link action by id. // Get a link action by id.
func (la *LinkAction) Get(client *Client) (*Action, *Response, error) { func (la *LinkAction) Get(ctx context.Context, client *Client) (*Action, *Response, error) {
return client.Actions.Get(la.ID) return client.Actions.Get(ctx, la.ID)
} }

275
vendor/github.com/digitalocean/godo/load_balancers.go generated vendored Normal file
View File

@ -0,0 +1,275 @@
package godo
import (
"context"
"fmt"
)
const loadBalancersBasePath = "/v2/load_balancers"
const forwardingRulesPath = "forwarding_rules"
const dropletsPath = "droplets"
// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
// See: https://developers.digitalocean.com/documentation/v2#load-balancers
type LoadBalancersService interface {
Get(context.Context, string) (*LoadBalancer, *Response, error)
List(context.Context, *ListOptions) ([]LoadBalancer, *Response, error)
Create(context.Context, *LoadBalancerRequest) (*LoadBalancer, *Response, error)
Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error)
Delete(ctx context.Context, lbID string) (*Response, error)
AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
}
// LoadBalancer represents a DigitalOcean load balancer configuration.
type LoadBalancer struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
IP string `json:"ip,omitempty"`
Algorithm string `json:"algorithm,omitempty"`
Status string `json:"status,omitempty"`
Created string `json:"created_at,omitempty"`
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
HealthCheck *HealthCheck `json:"health_check,omitempty"`
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
Region *Region `json:"region,omitempty"`
DropletIDs []int `json:"droplet_ids,omitempty"`
Tag string `json:"tag,omitempty"`
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
}
// String creates a human-readable description of a LoadBalancer.
func (l LoadBalancer) String() string {
return Stringify(l)
}
// ForwardingRule represents load balancer forwarding rules.
type ForwardingRule struct {
EntryProtocol string `json:"entry_protocol,omitempty"`
EntryPort int `json:"entry_port,omitempty"`
TargetProtocol string `json:"target_protocol,omitempty"`
TargetPort int `json:"target_port,omitempty"`
CertificateID string `json:"certificate_id,omitempty"`
TlsPassthrough bool `json:"tls_passthrough,omitempty"`
}
// String creates a human-readable description of a ForwardingRule.
func (f ForwardingRule) String() string {
return Stringify(f)
}
// HealthCheck represents optional load balancer health check rules.
type HealthCheck struct {
Protocol string `json:"protocol,omitempty"`
Port int `json:"port,omitempty"`
Path string `json:"path,omitempty"`
CheckIntervalSeconds int `json:"check_interval_seconds,omitempty"`
ResponseTimeoutSeconds int `json:"response_timeout_seconds,omitempty"`
HealthyThreshold int `json:"healthy_threshold,omitempty"`
UnhealthyThreshold int `json:"unhealthy_threshold,omitempty"`
}
// String creates a human-readable description of a HealthCheck.
func (h HealthCheck) String() string {
return Stringify(h)
}
// StickySessions represents optional load balancer session affinity rules.
type StickySessions struct {
Type string `json:"type,omitempty"`
CookieName string `json:"cookie_name,omitempty"`
CookieTtlSeconds int `json:"cookie_ttl_seconds,omitempty"`
}
// String creates a human-readable description of a StickySessions instance.
func (s StickySessions) String() string {
return Stringify(s)
}
// LoadBalancerRequest represents the configuration to be applied to an existing or a new load balancer.
type LoadBalancerRequest struct {
Name string `json:"name,omitempty"`
Algorithm string `json:"algorithm,omitempty"`
Region string `json:"region,omitempty"`
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
HealthCheck *HealthCheck `json:"health_check,omitempty"`
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
DropletIDs []int `json:"droplet_ids,omitempty"`
Tag string `json:"tag,omitempty"`
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
}
// String creates a human-readable description of a LoadBalancerRequest.
func (l LoadBalancerRequest) String() string {
return Stringify(l)
}
type forwardingRulesRequest struct {
Rules []ForwardingRule `json:"forwarding_rules,omitempty"`
}
func (l forwardingRulesRequest) String() string {
return Stringify(l)
}
type dropletIDsRequest struct {
IDs []int `json:"droplet_ids,omitempty"`
}
func (l dropletIDsRequest) String() string {
return Stringify(l)
}
type loadBalancersRoot struct {
LoadBalancers []LoadBalancer `json:"load_balancers"`
Links *Links `json:"links"`
}
type loadBalancerRoot struct {
LoadBalancer *LoadBalancer `json:"load_balancer"`
}
// LoadBalancersServiceOp handles communication with load balancer-related methods of the DigitalOcean API.
type LoadBalancersServiceOp struct {
client *Client
}
var _ LoadBalancersService = &LoadBalancersServiceOp{}
// Get an existing load balancer by its identifier.
func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
req, err := l.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(loadBalancerRoot)
resp, err := l.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.LoadBalancer, resp, err
}
// List load balancers, with optional pagination.
func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]LoadBalancer, *Response, error) {
path, err := addOptions(loadBalancersBasePath, opt)
if err != nil {
return nil, nil, err
}
req, err := l.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(loadBalancersRoot)
resp, err := l.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.LoadBalancers, resp, err
}
// Create a new load balancer with a given configuration.
func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
req, err := l.client.NewRequest(ctx, "POST", loadBalancersBasePath, lbr)
if err != nil {
return nil, nil, err
}
root := new(loadBalancerRoot)
resp, err := l.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.LoadBalancer, resp, err
}
// Update an existing load balancer with new configuration.
func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
req, err := l.client.NewRequest(ctx, "PUT", path, lbr)
if err != nil {
return nil, nil, err
}
root := new(loadBalancerRoot)
resp, err := l.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.LoadBalancer, resp, err
}
// Delete a load balancer by its identifier.
func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)
req, err := l.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
return l.client.Do(req, nil)
}
// AddDroplets adds droplets to a load balancer.
func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
req, err := l.client.NewRequest(ctx, "POST", path, &dropletIDsRequest{IDs: dropletIDs})
if err != nil {
return nil, err
}
return l.client.Do(req, nil)
}
// RemoveDroplets removes droplets from a load balancer.
func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
req, err := l.client.NewRequest(ctx, "DELETE", path, &dropletIDsRequest{IDs: dropletIDs})
if err != nil {
return nil, err
}
return l.client.Do(req, nil)
}
// AddForwardingRules adds forwarding rules to a load balancer.
func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
req, err := l.client.NewRequest(ctx, "POST", path, &forwardingRulesRequest{Rules: rules})
if err != nil {
return nil, err
}
return l.client.Do(req, nil)
}
// RemoveForwardingRules removes forwarding rules from a load balancer.
func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
req, err := l.client.NewRequest(ctx, "DELETE", path, &forwardingRulesRequest{Rules: rules})
if err != nil {
return nil, err
}
return l.client.Do(req, nil)
}

View File

@ -0,0 +1,796 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
var lbListJSONResponse = `
{
"load_balancers":[
{
"id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
"name":"example-lb-01",
"ip":"46.214.185.203",
"algorithm":"round_robin",
"status":"active",
"created_at":"2016-12-15T14:16:36Z",
"forwarding_rules":[
{
"entry_protocol":"https",
"entry_port":443,
"target_protocol":"http",
"target_port":80,
"certificate_id":"a-b-c"
}
],
"health_check":{
"protocol":"http",
"port":80,
"path":"/index.html",
"check_interval_seconds":10,
"response_timeout_seconds":5,
"healthy_threshold":5,
"unhealthy_threshold":3
},
"sticky_sessions":{
"type":"cookies",
"cookie_name":"DO-LB",
"cookie_ttl_seconds":5
},
"region":{
"name":"New York 1",
"slug":"nyc1",
"sizes":[
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb"
],
"features":[
"private_networking",
"backups",
"ipv6",
"metadata",
"storage"
],
"available":true
},
"droplet_ids":[
2,
21
]
}
],
"links":{
"pages":{
"last":"http://localhost:3001/v2/load_balancers?page=3&per_page=1",
"next":"http://localhost:3001/v2/load_balancers?page=2&per_page=1"
}
},
"meta":{
"total":3
}
}
`
var lbCreateJSONResponse = `
{
"load_balancer":{
"id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
"name":"example-lb-01",
"ip":"",
"algorithm":"round_robin",
"status":"new",
"created_at":"2016-12-15T14:19:09Z",
"forwarding_rules":[
{
"entry_protocol":"https",
"entry_port":443,
"target_protocol":"http",
"target_port":80,
"certificate_id":"a-b-c"
},
{
"entry_protocol":"https",
"entry_port":444,
"target_protocol":"https",
"target_port":443,
"tls_passthrough":true
}
],
"health_check":{
"protocol":"http",
"port":80,
"path":"/index.html",
"check_interval_seconds":10,
"response_timeout_seconds":5,
"healthy_threshold":5,
"unhealthy_threshold":3
},
"sticky_sessions":{
"type":"cookies",
"cookie_name":"DO-LB",
"cookie_ttl_seconds":5
},
"region":{
"name":"New York 1",
"slug":"nyc1",
"sizes":[
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb"
],
"features":[
"private_networking",
"backups",
"ipv6",
"metadata",
"storage"
],
"available":true
},
"droplet_ids":[
2,
21
],
"redirect_http_to_https":true
}
}
`
var lbGetJSONResponse = `
{
"load_balancer":{
"id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
"name":"example-lb-01",
"ip":"46.214.185.203",
"algorithm":"round_robin",
"status":"active",
"created_at":"2016-12-15T14:16:36Z",
"forwarding_rules":[
{
"entry_protocol":"https",
"entry_port":443,
"target_protocol":"http",
"target_port":80,
"certificate_id":"a-b-c"
}
],
"health_check":{
"protocol":"http",
"port":80,
"path":"/index.html",
"check_interval_seconds":10,
"response_timeout_seconds":5,
"healthy_threshold":5,
"unhealthy_threshold":3
},
"sticky_sessions":{
"type":"cookies",
"cookie_name":"DO-LB",
"cookie_ttl_seconds":5
},
"region":{
"name":"New York 1",
"slug":"nyc1",
"sizes":[
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb"
],
"features":[
"private_networking",
"backups",
"ipv6",
"metadata",
"storage"
],
"available":true
},
"droplet_ids":[
2,
21
]
}
}
`
var lbUpdateJSONResponse = `
{
"load_balancer":{
"id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
"name":"example-lb-01",
"ip":"12.34.56.78",
"algorithm":"least_connections",
"status":"active",
"created_at":"2016-12-15T14:19:09Z",
"forwarding_rules":[
{
"entry_protocol":"http",
"entry_port":80,
"target_protocol":"http",
"target_port":80
},
{
"entry_protocol":"https",
"entry_port":443,
"target_protocol":"http",
"target_port":80,
"certificate_id":"a-b-c"
}
],
"health_check":{
"protocol":"tcp",
"port":80,
"path":"",
"check_interval_seconds":10,
"response_timeout_seconds":5,
"healthy_threshold":5,
"unhealthy_threshold":3
},
"sticky_sessions":{
"type":"none"
},
"region":{
"name":"New York 1",
"slug":"nyc1",
"sizes":[
"512mb",
"1gb",
"2gb",
"4gb",
"8gb",
"16gb"
],
"features":[
"private_networking",
"backups",
"ipv6",
"metadata",
"storage"
],
"available":true
},
"droplet_ids":[
2,
21
]
}
}
`
func TestLoadBlanacers_Get(t *testing.T) {
setup()
defer teardown()
path := "/v2/load_balancers"
loadBalancerId := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path = fmt.Sprintf("%s/%s", path, loadBalancerId)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, lbGetJSONResponse)
})
loadBalancer, _, err := client.LoadBalancers.Get(ctx, loadBalancerId)
if err != nil {
t.Errorf("LoadBalancers.Get returned error: %v", err)
}
expected := &LoadBalancer{
ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057",
Name: "example-lb-01",
IP: "46.214.185.203",
Algorithm: "round_robin",
Status: "active",
Created: "2016-12-15T14:16:36Z",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
TlsPassthrough: false,
},
},
HealthCheck: &HealthCheck{
Protocol: "http",
Port: 80,
Path: "/index.html",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
HealthyThreshold: 5,
UnhealthyThreshold: 3,
},
StickySessions: &StickySessions{
Type: "cookies",
CookieName: "DO-LB",
CookieTtlSeconds: 5,
},
Region: &Region{
Slug: "nyc1",
Name: "New York 1",
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
Available: true,
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
},
DropletIDs: []int{2, 21},
}
assert.Equal(t, expected, loadBalancer)
}
func TestLoadBlanacers_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &LoadBalancerRequest{
Name: "example-lb-01",
Algorithm: "round_robin",
Region: "nyc1",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
},
},
HealthCheck: &HealthCheck{
Protocol: "http",
Port: 80,
Path: "/index.html",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
UnhealthyThreshold: 3,
HealthyThreshold: 5,
},
StickySessions: &StickySessions{
Type: "cookies",
CookieName: "DO-LB",
CookieTtlSeconds: 5,
},
Tag: "my-tag",
DropletIDs: []int{2, 21},
RedirectHttpToHttps: true,
}
path := "/v2/load_balancers"
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(LoadBalancerRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
assert.Equal(t, createRequest, v)
fmt.Fprint(w, lbCreateJSONResponse)
})
loadBalancer, _, err := client.LoadBalancers.Create(ctx, createRequest)
if err != nil {
t.Errorf("LoadBalancers.Create returned error: %v", err)
}
expected := &LoadBalancer{
ID: "8268a81c-fcf5-423e-a337-bbfe95817f23",
Name: "example-lb-01",
Algorithm: "round_robin",
Status: "new",
Created: "2016-12-15T14:19:09Z",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
TlsPassthrough: false,
},
{
EntryProtocol: "https",
EntryPort: 444,
TargetProtocol: "https",
TargetPort: 443,
CertificateID: "",
TlsPassthrough: true,
},
},
HealthCheck: &HealthCheck{
Protocol: "http",
Port: 80,
Path: "/index.html",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
HealthyThreshold: 5,
UnhealthyThreshold: 3,
},
StickySessions: &StickySessions{
Type: "cookies",
CookieName: "DO-LB",
CookieTtlSeconds: 5,
},
Region: &Region{
Slug: "nyc1",
Name: "New York 1",
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
Available: true,
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
},
DropletIDs: []int{2, 21},
RedirectHttpToHttps: true,
}
assert.Equal(t, expected, loadBalancer)
}
func TestLoadBlanacers_Update(t *testing.T) {
setup()
defer teardown()
updateRequest := &LoadBalancerRequest{
Name: "example-lb-01",
Algorithm: "least_connections",
Region: "nyc1",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "http",
EntryPort: 80,
TargetProtocol: "http",
TargetPort: 80,
},
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
},
},
HealthCheck: &HealthCheck{
Protocol: "tcp",
Port: 80,
Path: "",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
UnhealthyThreshold: 3,
HealthyThreshold: 5,
},
StickySessions: &StickySessions{
Type: "none",
},
DropletIDs: []int{2, 21},
}
path := "/v2/load_balancers"
loadBalancerId := "8268a81c-fcf5-423e-a337-bbfe95817f23"
path = fmt.Sprintf("%s/%s", path, loadBalancerId)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(LoadBalancerRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "PUT")
assert.Equal(t, updateRequest, v)
fmt.Fprint(w, lbUpdateJSONResponse)
})
loadBalancer, _, err := client.LoadBalancers.Update(ctx, loadBalancerId, updateRequest)
if err != nil {
t.Errorf("LoadBalancers.Update returned error: %v", err)
}
expected := &LoadBalancer{
ID: "8268a81c-fcf5-423e-a337-bbfe95817f23",
Name: "example-lb-01",
IP: "12.34.56.78",
Algorithm: "least_connections",
Status: "active",
Created: "2016-12-15T14:19:09Z",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "http",
EntryPort: 80,
TargetProtocol: "http",
TargetPort: 80,
},
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
},
},
HealthCheck: &HealthCheck{
Protocol: "tcp",
Port: 80,
Path: "",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
UnhealthyThreshold: 3,
HealthyThreshold: 5,
},
StickySessions: &StickySessions{
Type: "none",
},
Region: &Region{
Slug: "nyc1",
Name: "New York 1",
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
Available: true,
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
},
DropletIDs: []int{2, 21},
}
assert.Equal(t, expected, loadBalancer)
}
func TestLoadBlanacers_List(t *testing.T) {
setup()
defer teardown()
path := "/v2/load_balancers"
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, lbListJSONResponse)
})
loadBalancers, _, err := client.LoadBalancers.List(ctx, nil)
if err != nil {
t.Errorf("LoadBalancers.List returned error: %v", err)
}
expected := []LoadBalancer{
{
ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057",
Name: "example-lb-01",
IP: "46.214.185.203",
Algorithm: "round_robin",
Status: "active",
Created: "2016-12-15T14:16:36Z",
ForwardingRules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 443,
TargetProtocol: "http",
TargetPort: 80,
CertificateID: "a-b-c",
},
},
HealthCheck: &HealthCheck{
Protocol: "http",
Port: 80,
Path: "/index.html",
CheckIntervalSeconds: 10,
ResponseTimeoutSeconds: 5,
HealthyThreshold: 5,
UnhealthyThreshold: 3,
},
StickySessions: &StickySessions{
Type: "cookies",
CookieName: "DO-LB",
CookieTtlSeconds: 5,
},
Region: &Region{
Slug: "nyc1",
Name: "New York 1",
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
Available: true,
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
},
DropletIDs: []int{2, 21},
},
}
assert.Equal(t, expected, loadBalancers)
}
func TestLoadBlanacers_List_Pagination(t *testing.T) {
setup()
defer teardown()
path := "/v2/load_balancers"
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, map[string]string{"page": "2"})
fmt.Fprint(w, lbListJSONResponse)
})
opts := &ListOptions{Page: 2}
_, resp, err := client.LoadBalancers.List(ctx, opts)
if err != nil {
t.Errorf("LoadBalancers.List returned error: %v", err)
}
assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=2&per_page=1", resp.Links.Pages.Next)
assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=3&per_page=1", resp.Links.Pages.Last)
}
func TestLoadBlanacers_Delete(t *testing.T) {
setup()
defer teardown()
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path := "/v2/load_balancers"
path = fmt.Sprintf("%s/%s", path, lbID)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.LoadBalancers.Delete(ctx, lbID)
if err != nil {
t.Errorf("LoadBalancers.Delete returned error: %v", err)
}
}
func TestLoadBlanacers_AddDroplets(t *testing.T) {
setup()
defer teardown()
dropletIdsRequest := &dropletIDsRequest{
IDs: []int{42, 44},
}
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(dropletIDsRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
assert.Equal(t, dropletIdsRequest, v)
fmt.Fprint(w, nil)
})
_, err := client.LoadBalancers.AddDroplets(ctx, lbID, dropletIdsRequest.IDs...)
if err != nil {
t.Errorf("LoadBalancers.AddDroplets returned error: %v", err)
}
}
func TestLoadBlanacers_RemoveDroplets(t *testing.T) {
setup()
defer teardown()
dropletIdsRequest := &dropletIDsRequest{
IDs: []int{2, 21},
}
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(dropletIDsRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "DELETE")
assert.Equal(t, dropletIdsRequest, v)
fmt.Fprint(w, nil)
})
_, err := client.LoadBalancers.RemoveDroplets(ctx, lbID, dropletIdsRequest.IDs...)
if err != nil {
t.Errorf("LoadBalancers.RemoveDroplets returned error: %v", err)
}
}
func TestLoadBlanacers_AddForwardingRules(t *testing.T) {
setup()
defer teardown()
frr := &forwardingRulesRequest{
Rules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 444,
TargetProtocol: "http",
TargetPort: 81,
CertificateID: "b2abc00f-d3c4-426c-9f0b-b2f7a3ff7527",
},
{
EntryProtocol: "tcp",
EntryPort: 8080,
TargetProtocol: "tcp",
TargetPort: 8081,
},
},
}
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(forwardingRulesRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
assert.Equal(t, frr, v)
fmt.Fprint(w, nil)
})
_, err := client.LoadBalancers.AddForwardingRules(ctx, lbID, frr.Rules...)
if err != nil {
t.Errorf("LoadBalancers.AddForwardingRules returned error: %v", err)
}
}
func TestLoadBlanacers_RemoveForwardingRules(t *testing.T) {
setup()
defer teardown()
frr := &forwardingRulesRequest{
Rules: []ForwardingRule{
{
EntryProtocol: "https",
EntryPort: 444,
TargetProtocol: "http",
TargetPort: 81,
},
{
EntryProtocol: "tcp",
EntryPort: 8080,
TargetProtocol: "tcp",
TargetPort: 8081,
},
},
}
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
v := new(forwardingRulesRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "DELETE")
assert.Equal(t, frr, v)
fmt.Fprint(w, nil)
})
_, err := client.LoadBalancers.RemoveForwardingRules(ctx, lbID, frr.Rules...)
if err != nil {
t.Errorf("LoadBalancers.RemoveForwardingRules returned error: %v", err)
}
}

View File

@ -1,10 +1,12 @@
package godo package godo
import "context"
// RegionsService is an interface for interfacing with the regions // RegionsService is an interface for interfacing with the regions
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#regions // See: https://developers.digitalocean.com/documentation/v2#regions
type RegionsService interface { type RegionsService interface {
List(*ListOptions) ([]Region, *Response, error) List(context.Context, *ListOptions) ([]Region, *Response, error)
} }
// RegionsServiceOp handles communication with the region related methods of the // RegionsServiceOp handles communication with the region related methods of the
@ -29,23 +31,19 @@ type regionsRoot struct {
Links *Links `json:"links"` Links *Links `json:"links"`
} }
type regionRoot struct {
Region Region
}
func (r Region) String() string { func (r Region) String() string {
return Stringify(r) return Stringify(r)
} }
// List all regions // List all regions
func (s *RegionsServiceOp) List(opt *ListOptions) ([]Region, *Response, error) { func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region, *Response, error) {
path := "v2/regions" path := "v2/regions"
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -16,7 +16,7 @@ func TestRegions_List(t *testing.T) {
fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`) fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`)
}) })
regions, _, err := client.Regions.List(nil) regions, _, err := client.Regions.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Regions.List returned error: %v", err) t.Errorf("Regions.List returned error: %v", err)
} }
@ -36,7 +36,7 @@ func TestRegions_ListRegionsMultiplePages(t *testing.T) {
fmt.Fprint(w, `{"regions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/regions/?page=2"}}}`) fmt.Fprint(w, `{"regions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/regions/?page=2"}}}`)
}) })
_, resp, err := client.Regions.List(nil) _, resp, err := client.Regions.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -67,7 +67,7 @@ func TestRegions_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Regions.List(opt) _, resp, err := client.Regions.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,10 +1,12 @@
package godo package godo
import "context"
// SizesService is an interface for interfacing with the size // SizesService is an interface for interfacing with the size
// endpoints of the DigitalOcean API // endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#sizes // See: https://developers.digitalocean.com/documentation/v2#sizes
type SizesService interface { type SizesService interface {
List(*ListOptions) ([]Size, *Response, error) List(context.Context, *ListOptions) ([]Size, *Response, error)
} }
// SizesServiceOp handles communication with the size related methods of the // SizesServiceOp handles communication with the size related methods of the
@ -38,14 +40,14 @@ type sizesRoot struct {
} }
// List all images // List all images
func (s *SizesServiceOp) List(opt *ListOptions) ([]Size, *Response, error) { func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *Response, error) {
path := "v2/sizes" path := "v2/sizes"
path, err := addOptions(path, opt) path, err := addOptions(path, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req, err := s.client.NewRequest("GET", path, nil) req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -16,7 +16,7 @@ func TestSizes_List(t *testing.T) {
fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`) fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`)
}) })
sizes, _, err := client.Sizes.List(nil) sizes, _, err := client.Sizes.List(ctx, nil)
if err != nil { if err != nil {
t.Errorf("Sizes.List returned error: %v", err) t.Errorf("Sizes.List returned error: %v", err)
} }
@ -36,7 +36,7 @@ func TestSizes_ListSizesMultiplePages(t *testing.T) {
fmt.Fprint(w, `{"sizes": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/sizes/?page=2"}}}`) fmt.Fprint(w, `{"sizes": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/sizes/?page=2"}}}`)
}) })
_, resp, err := client.Sizes.List(nil) _, resp, err := client.Sizes.List(ctx, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -67,7 +67,7 @@ func TestSizes_RetrievePageByNumber(t *testing.T) {
}) })
opt := &ListOptions{Page: 2} opt := &ListOptions{Page: 2}
_, resp, err := client.Sizes.List(opt) _, resp, err := client.Sizes.List(ctx, opt)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

139
vendor/github.com/digitalocean/godo/snapshots.go generated vendored Normal file
View File

@ -0,0 +1,139 @@
package godo
import (
"context"
"fmt"
)
const snapshotBasePath = "v2/snapshots"
// SnapshotsService is an interface for interfacing with the snapshots
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#snapshots
type SnapshotsService interface {
List(context.Context, *ListOptions) ([]Snapshot, *Response, error)
ListVolume(context.Context, *ListOptions) ([]Snapshot, *Response, error)
ListDroplet(context.Context, *ListOptions) ([]Snapshot, *Response, error)
Get(context.Context, string) (*Snapshot, *Response, error)
Delete(context.Context, string) (*Response, error)
}
// SnapshotsServiceOp handles communication with the snapshot related methods of the
// DigitalOcean API.
type SnapshotsServiceOp struct {
client *Client
}
var _ SnapshotsService = &SnapshotsServiceOp{}
// Snapshot represents a DigitalOcean Snapshot
type Snapshot struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
ResourceID string `json:"resource_id,omitempty"`
ResourceType string `json:"resource_type,omitempty"`
Regions []string `json:"regions,omitempty"`
MinDiskSize int `json:"min_disk_size,omitempty"`
SizeGigaBytes float64 `json:"size_gigabytes,omitempty"`
Created string `json:"created_at,omitempty"`
}
type snapshotRoot struct {
Snapshot *Snapshot `json:"snapshot"`
}
type snapshotsRoot struct {
Snapshots []Snapshot `json:"snapshots"`
Links *Links `json:"links,omitempty"`
}
type listSnapshotOptions struct {
ResourceType string `url:"resource_type,omitempty"`
}
func (s Snapshot) String() string {
return Stringify(s)
}
// List lists all the snapshots available.
func (s *SnapshotsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
return s.list(ctx, opt, nil)
}
// ListDroplet lists all the Droplet snapshots.
func (s *SnapshotsServiceOp) ListDroplet(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
listOpt := listSnapshotOptions{ResourceType: "droplet"}
return s.list(ctx, opt, &listOpt)
}
// ListVolume lists all the volume snapshots.
func (s *SnapshotsServiceOp) ListVolume(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
listOpt := listSnapshotOptions{ResourceType: "volume"}
return s.list(ctx, opt, &listOpt)
}
// Get retrieves an snapshot by id.
func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snapshot, *Response, error) {
return s.get(ctx, snapshotID)
}
// Delete an snapshot.
func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID)
req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Helper method for getting an individual snapshot
func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s", snapshotBasePath, ID)
req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Snapshot, resp, err
}
// Helper method for listing snapshots
func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listSnapshotOptions) ([]Snapshot, *Response, error) {
path := snapshotBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
path, err = addOptions(path, listOpt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Snapshots, resp, err
}

186
vendor/github.com/digitalocean/godo/snapshots_test.go generated vendored Normal file
View File

@ -0,0 +1,186 @@
package godo
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestSnapshots_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`)
})
ctx := context.Background()
snapshots, _, err := client.Snapshots.List(ctx, nil)
if err != nil {
t.Errorf("Snapshots.List returned error: %v", err)
}
expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}}
if !reflect.DeepEqual(snapshots, expected) {
t.Errorf("Snapshots.List returned %+v, expected %+v", snapshots, expected)
}
}
func TestSnapshots_ListVolume(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
expected := "volume"
actual := r.URL.Query().Get("resource_type")
if actual != expected {
t.Errorf("'type' query = %v, expected %v", actual, expected)
}
fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2"}]}`)
})
ctx := context.Background()
snapshots, _, err := client.Snapshots.ListVolume(ctx, nil)
if err != nil {
t.Errorf("Snapshots.ListVolume returned error: %v", err)
}
expected := []Snapshot{{ID: "1"}, {ID: "2"}}
if !reflect.DeepEqual(snapshots, expected) {
t.Errorf("Snapshots.ListVolume returned %+v, expected %+v", snapshots, expected)
}
}
func TestSnapshots_ListDroplet(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
expected := "droplet"
actual := r.URL.Query().Get("resource_type")
if actual != expected {
t.Errorf("'resource_type' query = %v, expected %v", actual, expected)
}
fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`)
})
ctx := context.Background()
snapshots, _, err := client.Snapshots.ListDroplet(ctx, nil)
if err != nil {
t.Errorf("Snapshots.ListDroplet returned error: %v", err)
}
expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}}
if !reflect.DeepEqual(snapshots, expected) {
t.Errorf("Snapshots.ListDroplet returned %+v, expected %+v", snapshots, expected)
}
}
func TestSnapshots_ListSnapshotsMultiplePages(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"snapshots": [{"id":"1"},{"id":"2"}], "links":{"pages":{"next":"http://example.com/v2/snapshots/?page=2"}}}`)
})
ctx := context.Background()
_, resp, err := client.Snapshots.List(ctx, &ListOptions{Page: 2})
if err != nil {
t.Fatal(err)
}
checkCurrentPage(t, resp, 1)
}
func TestSnapshots_RetrievePageByNumber(t *testing.T) {
setup()
defer teardown()
jBlob := `
{
"snapshots": [{"id":"1"},{"id":"2"}],
"links":{
"pages":{
"next":"http://example.com/v2/snapshots/?page=3",
"prev":"http://example.com/v2/snapshots/?page=1",
"last":"http://example.com/v2/snapshots/?page=3",
"first":"http://example.com/v2/snapshots/?page=1"
}
}
}`
mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
ctx := context.Background()
opt := &ListOptions{Page: 2}
_, resp, err := client.Snapshots.List(ctx, opt)
if err != nil {
t.Fatal(err)
}
checkCurrentPage(t, resp, 2)
}
func TestSnapshots_GetSnapshotByID(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"snapshot":{"id":"12345"}}`)
})
ctx := context.Background()
snapshots, _, err := client.Snapshots.Get(ctx, "12345")
if err != nil {
t.Errorf("Snapshot.GetByID returned error: %v", err)
}
expected := &Snapshot{ID: "12345"}
if !reflect.DeepEqual(snapshots, expected) {
t.Errorf("Snapshots.GetByID returned %+v, expected %+v", snapshots, expected)
}
}
func TestSnapshots_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
ctx := context.Background()
_, err := client.Snapshots.Delete(ctx, "12345")
if err != nil {
t.Errorf("Snapshot.Delete returned error: %v", err)
}
}
func TestSnapshot_String(t *testing.T) {
snapshot := &Snapshot{
ID: "1",
Name: "Snapsh176ot",
ResourceID: "0",
ResourceType: "droplet",
Regions: []string{"one"},
MinDiskSize: 20,
SizeGigaBytes: 4.84,
Created: "2013-11-27T09:24:55Z",
}
stringified := snapshot.String()
expected := `godo.Snapshot{ID:"1", Name:"Snapsh176ot", ResourceID:"0", ResourceType:"droplet", Regions:["one"], MinDiskSize:20, SizeGigaBytes:4.84, Created:"2013-11-27T09:24:55Z"}`
if expected != stringified {
t.Errorf("Snapshot.String returned %+v, expected %+v", stringified, expected)
}
}

237
vendor/github.com/digitalocean/godo/storage.go generated vendored Normal file
View File

@ -0,0 +1,237 @@
package godo
import (
"context"
"fmt"
"time"
)
const (
storageBasePath = "v2"
storageAllocPath = storageBasePath + "/volumes"
storageSnapPath = storageBasePath + "/snapshots"
)
// StorageService is an interface for interfacing with the storage
// endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#storage
type StorageService interface {
ListVolumes(context.Context, *ListVolumeParams) ([]Volume, *Response, error)
GetVolume(context.Context, string) (*Volume, *Response, error)
CreateVolume(context.Context, *VolumeCreateRequest) (*Volume, *Response, error)
DeleteVolume(context.Context, string) (*Response, error)
ListSnapshots(ctx context.Context, volumeID string, opts *ListOptions) ([]Snapshot, *Response, error)
GetSnapshot(context.Context, string) (*Snapshot, *Response, error)
CreateSnapshot(context.Context, *SnapshotCreateRequest) (*Snapshot, *Response, error)
DeleteSnapshot(context.Context, string) (*Response, error)
}
// StorageServiceOp handles communication with the storage volumes related methods of the
// DigitalOcean API.
type StorageServiceOp struct {
client *Client
}
// ListVolumeParams stores the options you can set for a ListVolumeCall
type ListVolumeParams struct {
Region string `json:"region"`
Name string `json:"name"`
ListOptions *ListOptions `json:"list_options,omitempty"`
}
var _ StorageService = &StorageServiceOp{}
// Volume represents a Digital Ocean block store volume.
type Volume struct {
ID string `json:"id"`
Region *Region `json:"region"`
Name string `json:"name"`
SizeGigaBytes int64 `json:"size_gigabytes"`
Description string `json:"description"`
DropletIDs []int `json:"droplet_ids"`
CreatedAt time.Time `json:"created_at"`
}
func (f Volume) String() string {
return Stringify(f)
}
type storageVolumesRoot struct {
Volumes []Volume `json:"volumes"`
Links *Links `json:"links"`
}
type storageVolumeRoot struct {
Volume *Volume `json:"volume"`
Links *Links `json:"links,omitempty"`
}
// VolumeCreateRequest represents a request to create a block store
// volume.
type VolumeCreateRequest struct {
Region string `json:"region"`
Name string `json:"name"`
Description string `json:"description"`
SizeGigaBytes int64 `json:"size_gigabytes"`
}
// ListVolumes lists all storage volumes.
func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolumeParams) ([]Volume, *Response, error) {
path := storageAllocPath
if params != nil {
if params.Region != "" && params.Name != "" {
path = fmt.Sprintf("%s?name=%s&region=%s", path, params.Name, params.Region)
}
if params.ListOptions != nil {
var err error
path, err = addOptions(path, params.ListOptions)
if err != nil {
return nil, nil, err
}
}
}
req, err := svc.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumesRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Volumes, resp, nil
}
// CreateVolume creates a storage volume. The name must be unique.
func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
path := storageAllocPath
req, err := svc.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Volume, resp, nil
}
// GetVolume retrieves an individual storage volume.
func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
req, err := svc.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Volume, resp, nil
}
// DeleteVolume deletes a storage volume.
func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
req, err := svc.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
return svc.client.Do(req, nil)
}
// SnapshotCreateRequest represents a request to create a block store
// volume.
type SnapshotCreateRequest struct {
VolumeID string `json:"volume_id"`
Name string `json:"name"`
Description string `json:"description"`
}
// ListSnapshots lists all snapshots related to a storage volume.
func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, opt *ListOptions) ([]Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, volumeID)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := svc.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotsRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Snapshots, resp, nil
}
// CreateSnapshot creates a snapshot of a storage volume.
func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
req, err := svc.client.NewRequest(ctx, "POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Snapshot, resp, nil
}
// GetSnapshot retrieves an individual snapshot.
func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
req, err := svc.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Snapshot, resp, nil
}
// DeleteSnapshot deletes a snapshot.
func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
req, err := svc.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
return svc.client.Do(req, nil)
}

137
vendor/github.com/digitalocean/godo/storage_actions.go generated vendored Normal file
View File

@ -0,0 +1,137 @@
package godo
import (
"context"
"fmt"
)
// StorageActionsService is an interface for interfacing with the
// storage actions endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#storage-actions
type StorageActionsService interface {
Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
Detach(ctx context.Context, volumeID string) (*Action, *Response, error)
DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error)
List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error)
Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error)
}
// StorageActionsServiceOp handles communication with the storage volumes
// action related methods of the DigitalOcean API.
type StorageActionsServiceOp struct {
client *Client
}
// StorageAttachment represents the attachement of a block storage
// volume to a specific Droplet under the device name.
type StorageAttachment struct {
DropletID int `json:"droplet_id"`
}
// Attach a storage volume to a Droplet.
func (s *StorageActionsServiceOp) Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
request := &ActionRequest{
"type": "attach",
"droplet_id": dropletID,
}
return s.doAction(ctx, volumeID, request)
}
// Detach a storage volume from a Droplet.
func (s *StorageActionsServiceOp) Detach(ctx context.Context, volumeID string) (*Action, *Response, error) {
request := &ActionRequest{
"type": "detach",
}
return s.doAction(ctx, volumeID, request)
}
// DetachByDropletID a storage volume from a Droplet by Droplet ID.
func (s *StorageActionsServiceOp) DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
request := &ActionRequest{
"type": "detach",
"droplet_id": dropletID,
}
return s.doAction(ctx, volumeID, request)
}
// Get an action for a particular storage volume by id.
func (s *StorageActionsServiceOp) Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error) {
path := fmt.Sprintf("%s/%d", storageAllocationActionPath(volumeID), actionID)
return s.get(ctx, path)
}
// List the actions for a particular storage volume.
func (s *StorageActionsServiceOp) List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error) {
path := storageAllocationActionPath(volumeID)
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(ctx, path)
}
// Resize a storage volume.
func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error) {
request := &ActionRequest{
"type": "resize",
"size_gigabytes": sizeGigabytes,
"region": regionSlug,
}
return s.doAction(ctx, volumeID, request)
}
func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) {
path := storageAllocationActionPath(volumeID)
req, err := s.client.NewRequest(ctx, "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
}
func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
req, err := s.client.NewRequest(ctx, "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 (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
req, err := s.client.NewRequest(ctx, "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
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Actions, resp, err
}
func storageAllocationActionPath(volumeID string) string {
return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID)
}

View File

@ -0,0 +1,179 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestStoragesActions_Attach(t *testing.T) {
setup()
defer teardown()
const (
volumeID = "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
dropletID = 12345
)
attachRequest := &ActionRequest{
"type": "attach",
"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
}
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, attachRequest) {
t.Errorf("want=%#v", attachRequest)
t.Errorf("got=%#v", v)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
_, _, err := client.StorageActions.Attach(ctx, volumeID, dropletID)
if err != nil {
t.Errorf("StoragesActions.Attach returned error: %v", err)
}
}
func TestStoragesActions_Detach(t *testing.T) {
setup()
defer teardown()
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
detachRequest := &ActionRequest{
"type": "detach",
}
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, detachRequest) {
t.Errorf("want=%#v", detachRequest)
t.Errorf("got=%#v", v)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
_, _, err := client.StorageActions.Detach(ctx, volumeID)
if err != nil {
t.Errorf("StoragesActions.Detach returned error: %v", err)
}
}
func TestStoragesActions_DetachByDropletID(t *testing.T) {
setup()
defer teardown()
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
dropletID := 123456
detachByDropletIDRequest := &ActionRequest{
"type": "detach",
"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
}
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, detachByDropletIDRequest) {
t.Errorf("want=%#v", detachByDropletIDRequest)
t.Errorf("got=%#v", v)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
_, _, err := client.StorageActions.DetachByDropletID(ctx, volumeID, dropletID)
if err != nil {
t.Errorf("StoragesActions.DetachByDropletID returned error: %v", err)
}
}
func TestStorageActions_Get(t *testing.T) {
setup()
defer teardown()
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions/456", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
action, _, err := client.StorageActions.Get(ctx, volumeID, 456)
if err != nil {
t.Errorf("StorageActions.Get returned error: %v", err)
}
expected := &Action{Status: "in-progress"}
if !reflect.DeepEqual(action, expected) {
t.Errorf("StorageActions.Get returned %+v, expected %+v", action, expected)
}
}
func TestStorageActions_List(t *testing.T) {
setup()
defer teardown()
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprintf(w, `{"actions":[{"status":"in-progress"}]}`)
})
actions, _, err := client.StorageActions.List(ctx, volumeID, nil)
if err != nil {
t.Errorf("StorageActions.List returned error: %v", err)
}
expected := []Action{{Status: "in-progress"}}
if !reflect.DeepEqual(actions, expected) {
t.Errorf("StorageActions.List returned %+v, expected %+v", actions, expected)
}
}
func TestStoragesActions_Resize(t *testing.T) {
setup()
defer teardown()
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
resizeRequest := &ActionRequest{
"type": "resize",
"size_gigabytes": float64(500),
"region": "nyc1",
}
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
v := new(ActionRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, resizeRequest) {
t.Errorf("want=%#v", resizeRequest)
t.Errorf("got=%#v", v)
}
fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`)
})
_, _, err := client.StorageActions.Resize(ctx, volumeID, 500, "nyc1")
if err != nil {
t.Errorf("StoragesActions.Resize returned error: %v", err)
}
}

436
vendor/github.com/digitalocean/godo/storage_test.go generated vendored Normal file
View File

@ -0,0 +1,436 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
"time"
)
func TestStorageVolumes_ListStorageVolumes(t *testing.T) {
setup()
defer teardown()
jBlob := `
{
"volumes": [
{
"user_id": 42,
"region": {"slug": "nyc3"},
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my volume",
"description": "my description",
"size_gigabytes": 100,
"droplet_ids": [10],
"created_at": "2002-10-02T15:00:00.05Z"
},
{
"user_id": 42,
"region": {"slug": "nyc3"},
"id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
"name": "my other volume",
"description": "my other description",
"size_gigabytes": 100,
"created_at": "2012-10-03T15:00:01.05Z"
}
],
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/volumes?page=2",
"next": "https://api.digitalocean.com/v2/volumes?page=2"
}
},
"meta": {
"total": 28
}
}`
mux.HandleFunc("/v2/volumes/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
volumes, _, err := client.Storage.ListVolumes(ctx, nil)
if err != nil {
t.Errorf("Storage.ListVolumes returned error: %v", err)
}
expected := []Volume{
{
Region: &Region{Slug: "nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my volume",
Description: "my description",
SizeGigaBytes: 100,
DropletIDs: []int{10},
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
},
{
Region: &Region{Slug: "nyc3"},
ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
Name: "my other volume",
Description: "my other description",
SizeGigaBytes: 100,
CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC),
},
}
if !reflect.DeepEqual(volumes, expected) {
t.Errorf("Storage.ListVolumes returned %+v, expected %+v", volumes, expected)
}
}
func TestStorageVolumes_Get(t *testing.T) {
setup()
defer teardown()
want := &Volume{
Region: &Region{Slug: "nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my volume",
Description: "my description",
SizeGigaBytes: 100,
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
}
jBlob := `{
"volume":{
"region": {"slug":"nyc3"},
"attached_to_droplet": null,
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my volume",
"description": "my description",
"size_gigabytes": 100,
"created_at": "2002-10-02T15:00:00.05Z"
},
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/volumes?page=2",
"next": "https://api.digitalocean.com/v2/volumes?page=2"
}
},
"meta": {
"total": 28
}
}`
mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
got, _, err := client.Storage.GetVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
if err != nil {
t.Errorf("Storage.GetVolume returned error: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Storage.GetVolume returned %+v, want %+v", got, want)
}
}
func TestStorageVolumes_ListVolumesByName(t *testing.T) {
setup()
defer teardown()
jBlob :=
`{
"volumes": [
{
"region": {"slug": "nyc3"},
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "myvolume",
"description": "my description",
"size_gigabytes": 100,
"droplet_ids": [10],
"created_at": "2002-10-02T15:00:00.05Z"
}
],
"links": {},
"meta": {
"total": 1
}
}`
expected := []Volume{
{
Region: &Region{Slug: "nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "myvolume",
Description: "my description",
SizeGigaBytes: 100,
DropletIDs: []int{10},
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
},
}
mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("name") != "myvolume" || r.URL.Query().Get("region") != "nyc3" {
t.Errorf("Storage.GetVolumeByName did not request the correct name or region")
}
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
options := &ListVolumeParams{
Name: "myvolume",
Region: "nyc3",
}
volumes, _, err := client.Storage.ListVolumes(ctx, options)
if err != nil {
t.Errorf("Storage.GetVolumeByName returned error: %v", err)
}
if !reflect.DeepEqual(volumes, expected) {
t.Errorf("Storage.GetVolumeByName returned %+v, expected %+v", volumes, expected)
}
}
func TestStorageVolumes_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &VolumeCreateRequest{
Region: "nyc3",
Name: "my volume",
Description: "my description",
SizeGigaBytes: 100,
}
want := &Volume{
Region: &Region{Slug: "nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my volume",
Description: "my description",
SizeGigaBytes: 100,
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
}
jBlob := `{
"volume":{
"region": {"slug":"nyc3"},
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my volume",
"description": "my description",
"size_gigabytes": 100,
"created_at": "2002-10-02T15:00:00.05Z"
},
"links": {}
}`
mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
v := new(VolumeCreateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprint(w, jBlob)
})
got, _, err := client.Storage.CreateVolume(ctx, createRequest)
if err != nil {
t.Errorf("Storage.CreateVolume returned error: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want)
}
}
func TestStorageVolumes_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Storage.DeleteVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
if err != nil {
t.Errorf("Storage.DeleteVolume returned error: %v", err)
}
}
func TestStorageSnapshots_ListStorageSnapshots(t *testing.T) {
setup()
defer teardown()
jBlob := `
{
"snapshots": [
{
"regions": ["nyc3"],
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my snapshot",
"size_gigabytes": 100,
"created_at": "2002-10-02T15:00:00.05Z"
},
{
"regions": ["nyc3"],
"id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
"name": "my other snapshot",
"size_gigabytes": 100,
"created_at": "2012-10-03T15:00:01.05Z"
}
],
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/volumes?page=2",
"next": "https://api.digitalocean.com/v2/volumes?page=2"
}
},
"meta": {
"total": 28
}
}`
mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
volumes, _, err := client.Storage.ListSnapshots(ctx, "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", nil)
if err != nil {
t.Errorf("Storage.ListSnapshots returned error: %v", err)
}
expected := []Snapshot{
{
Regions: []string{"nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my snapshot",
SizeGigaBytes: 100,
Created: "2002-10-02T15:00:00.05Z",
},
{
Regions: []string{"nyc3"},
ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
Name: "my other snapshot",
SizeGigaBytes: 100,
Created: "2012-10-03T15:00:01.05Z",
},
}
if !reflect.DeepEqual(volumes, expected) {
t.Errorf("Storage.ListSnapshots returned %+v, expected %+v", volumes, expected)
}
}
func TestStorageSnapshots_Get(t *testing.T) {
setup()
defer teardown()
want := &Snapshot{
Regions: []string{"nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my snapshot",
SizeGigaBytes: 100,
Created: "2002-10-02T15:00:00.05Z",
}
jBlob := `{
"snapshot":{
"regions": ["nyc3"],
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my snapshot",
"size_gigabytes": 100,
"created_at": "2002-10-02T15:00:00.05Z"
},
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
"next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
}
},
"meta": {
"total": 28
}
}`
mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, jBlob)
})
got, _, err := client.Storage.GetSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
if err != nil {
t.Errorf("Storage.GetSnapshot returned error: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Storage.GetSnapshot returned %+v, want %+v", got, want)
}
}
func TestStorageSnapshots_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &SnapshotCreateRequest{
VolumeID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my snapshot",
Description: "my description",
}
want := &Snapshot{
Regions: []string{"nyc3"},
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
Name: "my snapshot",
SizeGigaBytes: 100,
Created: "2002-10-02T15:00:00.05Z",
}
jBlob := `{
"snapshot":{
"regions": ["nyc3"],
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
"name": "my snapshot",
"description": "my description",
"size_gigabytes": 100,
"created_at": "2002-10-02T15:00:00.05Z"
},
"links": {
"pages": {
"last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
"next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
}
},
"meta": {
"total": 28
}
}`
mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) {
v := new(SnapshotCreateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatal(err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprint(w, jBlob)
})
got, _, err := client.Storage.CreateSnapshot(ctx, createRequest)
if err != nil {
t.Errorf("Storage.CreateSnapshot returned error: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Storage.CreateSnapshot returned %+v, want %+v", got, want)
}
}
func TestStorageSnapshots_Destroy(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Storage.DeleteSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1")
if err != nil {
t.Errorf("Storage.DeleteSnapshot returned error: %v", err)
}
}

View File

@ -3,6 +3,7 @@ package godo
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"reflect" "reflect"
) )
@ -17,7 +18,7 @@ func Stringify(message interface{}) string {
} }
// stringifyValue was graciously cargoculted from the goprotubuf library // stringifyValue was graciously cargoculted from the goprotubuf library
func stringifyValue(w *bytes.Buffer, val reflect.Value) { func stringifyValue(w io.Writer, val reflect.Value) {
if val.Kind() == reflect.Ptr && val.IsNil() { if val.Kind() == reflect.Ptr && val.IsNil() {
_, _ = w.Write([]byte("<nil>")) _, _ = w.Write([]byte("<nil>"))
return return
@ -29,6 +30,18 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) {
case reflect.String: case reflect.String:
fmt.Fprintf(w, `"%s"`, v) fmt.Fprintf(w, `"%s"`, v)
case reflect.Slice: case reflect.Slice:
stringifySlice(w, v)
return
case reflect.Struct:
stringifyStruct(w, v)
default:
if v.CanInterface() {
fmt.Fprint(w, v.Interface())
}
}
}
func stringifySlice(w io.Writer, v reflect.Value) {
_, _ = w.Write([]byte{'['}) _, _ = w.Write([]byte{'['})
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
if i > 0 { if i > 0 {
@ -39,8 +52,9 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) {
} }
_, _ = w.Write([]byte{']'}) _, _ = w.Write([]byte{']'})
return }
case reflect.Struct:
func stringifyStruct(w io.Writer, v reflect.Value) {
if v.Type().Name() != "" { if v.Type().Name() != "" {
_, _ = w.Write([]byte(v.Type().String())) _, _ = w.Write([]byte(v.Type().String()))
} }
@ -75,9 +89,4 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) {
} }
_, _ = w.Write([]byte{'}'}) _, _ = w.Write([]byte{'}'})
default:
if v.CanInterface() {
fmt.Fprint(w, v.Interface())
}
}
} }

234
vendor/github.com/digitalocean/godo/tags.go generated vendored Normal file
View File

@ -0,0 +1,234 @@
package godo
import (
"context"
"fmt"
)
const tagsBasePath = "v2/tags"
// TagsService is an interface for interfacing with the tags
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#tags
type TagsService interface {
List(context.Context, *ListOptions) ([]Tag, *Response, error)
Get(context.Context, string) (*Tag, *Response, error)
Create(context.Context, *TagCreateRequest) (*Tag, *Response, error)
Update(context.Context, string, *TagUpdateRequest) (*Response, error)
Delete(context.Context, string) (*Response, error)
TagResources(context.Context, string, *TagResourcesRequest) (*Response, error)
UntagResources(context.Context, string, *UntagResourcesRequest) (*Response, error)
}
// TagsServiceOp handles communication with tag related method of the
// DigitalOcean API.
type TagsServiceOp struct {
client *Client
}
var _ TagsService = &TagsServiceOp{}
// ResourceType represents a class of resource, currently only droplet are supported
type ResourceType string
const (
//DropletResourceType holds the string representing our ResourceType of Droplet.
DropletResourceType ResourceType = "droplet"
)
// Resource represent a single resource for associating/disassociating with tags
type Resource struct {
ID string `json:"resource_id,omit_empty"`
Type ResourceType `json:"resource_type,omit_empty"`
}
// TaggedResources represent the set of resources a tag is attached to
type TaggedResources struct {
Droplets *TaggedDropletsResources `json:"droplets,omitempty"`
}
// TaggedDropletsResources represent the droplet resources a tag is attached to
type TaggedDropletsResources struct {
Count int `json:"count,float64,omitempty"`
LastTagged *Droplet `json:"last_tagged,omitempty"`
}
// Tag represent DigitalOcean tag
type Tag struct {
Name string `json:"name,omitempty"`
Resources *TaggedResources `json:"resources,omitempty"`
}
//TagCreateRequest represents the JSON structure of a request of that type.
type TagCreateRequest struct {
Name string `json:"name"`
}
//TagUpdateRequest represents the JSON structure of a request of that type.
type TagUpdateRequest struct {
Name string `json:"name"`
}
// TagResourcesRequest represents the JSON structure of a request of that type.
type TagResourcesRequest struct {
Resources []Resource `json:"resources"`
}
// UntagResourcesRequest represents the JSON structure of a request of that type.
type UntagResourcesRequest struct {
Resources []Resource `json:"resources"`
}
type tagsRoot struct {
Tags []Tag `json:"tags"`
Links *Links `json:"links"`
}
type tagRoot struct {
Tag *Tag `json:"tag"`
}
// List all tags
func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Response, error) {
path := tagsBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Tags, resp, err
}
// Get a single tag
func (s *TagsServiceOp) Get(ctx context.Context, name string) (*Tag, *Response, error) {
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
req, err := s.client.NewRequest(ctx, "GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Tag, resp, err
}
// Create a new tag
func (s *TagsServiceOp) Create(ctx context.Context, createRequest *TagCreateRequest) (*Tag, *Response, error) {
if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil")
}
req, err := s.client.NewRequest(ctx, "POST", tagsBasePath, createRequest)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Tag, resp, err
}
// Update an exsting tag
func (s *TagsServiceOp) Update(ctx context.Context, name string, updateRequest *TagUpdateRequest) (*Response, error) {
if name == "" {
return nil, NewArgError("name", "cannot be empty")
}
if updateRequest == nil {
return nil, NewArgError("updateRequest", "cannot be nil")
}
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Delete an existing tag
func (s *TagsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
if name == "" {
return nil, NewArgError("name", "cannot be empty")
}
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// TagResources associates resources with a given Tag.
func (s *TagsServiceOp) TagResources(ctx context.Context, name string, tagRequest *TagResourcesRequest) (*Response, error) {
if name == "" {
return nil, NewArgError("name", "cannot be empty")
}
if tagRequest == nil {
return nil, NewArgError("tagRequest", "cannot be nil")
}
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
req, err := s.client.NewRequest(ctx, "POST", path, tagRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// UntagResources dissociates resources with a given Tag.
func (s *TagsServiceOp) UntagResources(ctx context.Context, name string, untagRequest *UntagResourcesRequest) (*Response, error) {
if name == "" {
return nil, NewArgError("name", "cannot be empty")
}
if untagRequest == nil {
return nil, NewArgError("tagRequest", "cannot be nil")
}
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
req, err := s.client.NewRequest(ctx, "DELETE", path, untagRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}

384
vendor/github.com/digitalocean/godo/tags_test.go generated vendored Normal file
View File

@ -0,0 +1,384 @@
package godo
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
var (
listEmptyJSON = `
{
"tags": [
],
"meta": {
"total": 0
}
}
`
listJSON = `
{
"tags": [
{
"name": "testing-1",
"resources": {
"droplets": {
"count": 0,
"last_tagged": null
}
}
},
{
"name": "testing-2",
"resources": {
"droplets": {
"count": 0,
"last_tagged": null
}
}
}
],
"links": {
"pages":{
"next":"http://example.com/v2/tags/?page=3",
"prev":"http://example.com/v2/tags/?page=1",
"last":"http://example.com/v2/tags/?page=3",
"first":"http://example.com/v2/tags/?page=1"
}
},
"meta": {
"total": 2
}
}
`
createJSON = `
{
"tag": {
"name": "testing-1",
"resources": {
"droplets": {
"count": 0,
"last_tagged": null
}
}
}
}
`
getJSON = `
{
"tag": {
"name": "testing-1",
"resources": {
"droplets": {
"count": 1,
"last_tagged": {
"id": 1,
"name": "test.example.com",
"memory": 1024,
"vcpus": 2,
"disk": 20,
"region": {
"slug": "nyc1",
"name": "New York",
"sizes": [
"1024mb",
"512mb"
],
"available": true,
"features": [
"virtio",
"private_networking",
"backups",
"ipv6"
]
},
"image": {
"id": 119192817,
"name": "Ubuntu 13.04",
"distribution": "ubuntu",
"slug": "ubuntu1304",
"public": true,
"regions": [
"nyc1"
],
"created_at": "2014-07-29T14:35:37Z"
},
"size_slug": "1024mb",
"locked": false,
"status": "active",
"networks": {
"v4": [
{
"ip_address": "10.0.0.19",
"netmask": "255.255.0.0",
"gateway": "10.0.0.1",
"type": "private"
},
{
"ip_address": "127.0.0.19",
"netmask": "255.255.255.0",
"gateway": "127.0.0.20",
"type": "public"
}
],
"v6": [
{
"ip_address": "2001::13",
"cidr": 124,
"gateway": "2400:6180:0000:00D0:0000:0000:0009:7000",
"type": "public"
}
]
},
"kernel": {
"id": 485432985,
"name": "DO-recovery-static-fsck",
"version": "3.8.0-25-generic"
},
"created_at": "2014-07-29T14:35:37Z",
"features": [
"ipv6"
],
"backup_ids": [
449676382
],
"snapshot_ids": [
449676383
],
"action_ids": [
],
"tags": [
"tag-1",
"tag-2"
]
}
}
}
}
}
`
)
func TestTags_List(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, listJSON)
})
tags, _, err := client.Tags.List(ctx, nil)
if err != nil {
t.Errorf("Tags.List returned error: %v", err)
}
expected := []Tag{{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}},
{Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}}
if !reflect.DeepEqual(tags, expected) {
t.Errorf("Tags.List returned %+v, expected %+v", tags, expected)
}
}
func TestTags_ListEmpty(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, listEmptyJSON)
})
tags, _, err := client.Tags.List(ctx, nil)
if err != nil {
t.Errorf("Tags.List returned error: %v", err)
}
expected := []Tag{}
if !reflect.DeepEqual(tags, expected) {
t.Errorf("Tags.List returned %+v, expected %+v", tags, expected)
}
}
func TestTags_ListPaging(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, listJSON)
})
_, resp, err := client.Tags.List(ctx, nil)
if err != nil {
t.Errorf("Tags.List returned error: %v", err)
}
checkCurrentPage(t, resp, 2)
}
func TestTags_Get(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, getJSON)
})
tag, _, err := client.Tags.Get(ctx, "testing-1")
if err != nil {
t.Errorf("Tags.Get returned error: %v", err)
}
if tag.Name != "testing-1" {
t.Errorf("Tags.Get return an incorrect name, got %+v, expected %+v", tag.Name, "testing-1")
}
if tag.Resources.Droplets.Count != 1 {
t.Errorf("Tags.Get return an incorrect droplet resource count, got %+v, expected %+v", tag.Resources.Droplets.Count, 1)
}
if tag.Resources.Droplets.LastTagged.ID != 1 {
t.Errorf("Tags.Get return an incorrect last tagged droplet %+v, expected %+v", tag.Resources.Droplets.LastTagged.ID, 1)
}
}
func TestTags_Create(t *testing.T) {
setup()
defer teardown()
createRequest := &TagCreateRequest{
Name: "testing-1",
}
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
v := new(TagCreateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, createRequest) {
t.Errorf("Request body = %+v, expected %+v", v, createRequest)
}
fmt.Fprintf(w, createJSON)
})
tag, _, err := client.Tags.Create(ctx, createRequest)
if err != nil {
t.Errorf("Tags.Create returned error: %v", err)
}
expected := &Tag{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}
if !reflect.DeepEqual(tag, expected) {
t.Errorf("Tags.Create returned %+v, expected %+v", tag, expected)
}
}
func TestTags_Update(t *testing.T) {
setup()
defer teardown()
updateRequest := &TagUpdateRequest{
Name: "testing-1",
}
mux.HandleFunc("/v2/tags/old-testing-1", func(w http.ResponseWriter, r *http.Request) {
v := new(TagUpdateRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "PUT")
if !reflect.DeepEqual(v, updateRequest) {
t.Errorf("Request body = %+v, expected %+v", v, updateRequest)
}
})
_, err := client.Tags.Update(ctx, "old-testing-1", updateRequest)
if err != nil {
t.Errorf("Tags.Update returned error: %v", err)
}
}
func TestTags_Delete(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
})
_, err := client.Tags.Delete(ctx, "testing-1")
if err != nil {
t.Errorf("Tags.Delete returned error: %v", err)
}
}
func TestTags_TagResource(t *testing.T) {
setup()
defer teardown()
tagResourcesRequest := &TagResourcesRequest{
Resources: []Resource{{ID: "1", Type: DropletResourceType}},
}
mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) {
v := new(TagResourcesRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, tagResourcesRequest) {
t.Errorf("Request body = %+v, expected %+v", v, tagResourcesRequest)
}
})
_, err := client.Tags.TagResources(ctx, "testing-1", tagResourcesRequest)
if err != nil {
t.Errorf("Tags.TagResources returned error: %v", err)
}
}
func TestTags_UntagResource(t *testing.T) {
setup()
defer teardown()
untagResourcesRequest := &UntagResourcesRequest{
Resources: []Resource{{ID: "1", Type: DropletResourceType}},
}
mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) {
v := new(UntagResourcesRequest)
err := json.NewDecoder(r.Body).Decode(v)
if err != nil {
t.Fatalf("decode json: %v", err)
}
testMethod(t, r, "DELETE")
if !reflect.DeepEqual(v, untagResourcesRequest) {
t.Errorf("Request body = %+v, expected %+v", v, untagResourcesRequest)
}
})
_, err := client.Tags.UntagResources(ctx, "testing-1", untagResourcesRequest)
if err != nil {
t.Errorf("Tags.UntagResources returned error: %v", err)
}
}

View File

@ -88,6 +88,9 @@ func TestTimstamp_MarshalReflexivity(t *testing.T) {
} }
var got Timestamp var got Timestamp
err = json.Unmarshal(data, &got) err = json.Unmarshal(data, &got)
if err != nil {
t.Errorf("%s: Unmarshal err=%v", data, err)
}
if !got.Equal(tc.data) { if !got.Equal(tc.data) {
t.Errorf("%s: %+v != %+v", tc.desc, got, data) t.Errorf("%s: %+v != %+v", tc.desc, got, data)
} }
@ -169,6 +172,9 @@ func TestWrappedTimestamp_MarshalReflexivity(t *testing.T) {
} }
var got WrappedTimestamp var got WrappedTimestamp
err = json.Unmarshal(bytes, &got) err = json.Unmarshal(bytes, &got)
if err != nil {
t.Errorf("%s: Unmarshal err=%v", bytes, err)
}
if !got.Time.Equal(tc.data.Time) { if !got.Time.Equal(tc.data.Time) {
t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data) t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data)
} }

View File

@ -1,6 +1,7 @@
package util package util
import ( import (
"context"
"fmt" "fmt"
"time" "time"
@ -15,7 +16,7 @@ const (
) )
// WaitForActive waits for a droplet to become active // WaitForActive waits for a droplet to become active
func WaitForActive(client *godo.Client, monitorURI string) error { func WaitForActive(ctx context.Context, client *godo.Client, monitorURI string) error {
if len(monitorURI) == 0 { if len(monitorURI) == 0 {
return fmt.Errorf("create had no monitor uri") return fmt.Errorf("create had no monitor uri")
} }
@ -23,9 +24,14 @@ func WaitForActive(client *godo.Client, monitorURI string) error {
completed := false completed := false
failCount := 0 failCount := 0
for !completed { for !completed {
action, _, err := client.DropletActions.GetByURI(monitorURI) action, _, err := client.DropletActions.GetByURI(ctx, monitorURI)
if err != nil { if err != nil {
select {
case <-ctx.Done():
return err
default:
}
if failCount <= activeFailure { if failCount <= activeFailure {
failCount++ failCount++
continue continue
@ -35,7 +41,11 @@ func WaitForActive(client *godo.Client, monitorURI string) error {
switch action.Status { switch action.Status {
case godo.ActionInProgress: case godo.ActionInProgress:
time.Sleep(5 * time.Second) select {
case <-time.After(5 * time.Second):
case <-ctx.Done():
return err
}
case godo.ActionCompleted: case godo.ActionCompleted:
completed = true completed = true
default: default:

View File

@ -1,6 +1,8 @@
package util package util
import ( import (
"context"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/digitalocean/godo" "github.com/digitalocean/godo"
@ -19,7 +21,7 @@ func ExampleWaitForActive() {
uri := "https://api.digitalocean.com/v2/actions/xxxxxxxx" uri := "https://api.digitalocean.com/v2/actions/xxxxxxxx"
// block until until the action is complete // block until until the action is complete
err := WaitForActive(client, uri) err := WaitForActive(context.TODO(), client, uri)
if err != nil { if err != nil {
panic(err) panic(err)
} }