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
@ -17,10 +19,13 @@ 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"`
Email string `json:"email,omitempty"` FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
UUID string `json:"uuid,omitempty"` Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"` UUID string `json:"uuid,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)
@ -40,14 +41,17 @@ func TestAccountGet(t *testing.T) {
func TestAccountString(t *testing.T) { func TestAccountString(t *testing.T) {
acct := &Account{ acct := &Account{
DropletLimit: 25, DropletLimit: 25,
Email: "sammy@digitalocean.com", FloatingIPLimit: 25,
UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", Email: "sammy@digitalocean.com",
EmailVerified: true, UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef",
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
@ -32,22 +39,71 @@ var _ DropletsService = &DropletsServiceOp{}
// Droplet represents a DigitalOcean Droplet // Droplet represents a DigitalOcean Droplet
type Droplet struct { type Droplet struct {
ID int `json:"id,float64,omitempty"` ID int `json:"id,float64,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Memory int `json:"memory,omitempty"` Memory int `json:"memory,omitempty"`
Vcpus int `json:"vcpus,omitempty"` Vcpus int `json:"vcpus,omitempty"`
Disk int `json:"disk,omitempty"` Disk int `json:"disk,omitempty"`
Region *Region `json:"region,omitempty"` Region *Region `json:"region,omitempty"`
Image *Image `json:"image,omitempty"` Image *Image `json:"image,omitempty"`
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"`
SnapshotIDs []int `json:"snapshot_ids,omitempty"` NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"`
Locked bool `json:"locked,bool,omitempty"` SnapshotIDs []int `json:"snapshot_ids,omitempty"`
Status string `json:"status,omitempty"` Features []string `json:"features,omitempty"`
Networks *Networks `json:"networks,omitempty"` Locked bool `json:"locked,bool,omitempty"`
ActionIDs []int `json:"action_ids,omitempty"` Status string `json:"status,omitempty"`
Created string `json:"created_at,omitempty"` Networks *Networks `json:"networks,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.
@ -43,16 +44,24 @@ type Client struct {
Rate Rate Rate Rate
// Services used for communicating with the API // Services used for communicating with the API
Account AccountService Account AccountService
Actions ActionsService Actions ActionsService
Domains DomainsService Domains DomainsService
Droplets DropletsService Droplets DropletsService
DropletActions DropletActionsService DropletActions DropletActionsService
Images ImagesService Images ImagesService
ImageActions ImageActionsService ImageActions ImageActionsService
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,55 +30,63 @@ 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:
_, _ = w.Write([]byte{'['}) stringifySlice(w, v)
for i := 0; i < v.Len(); i++ {
if i > 0 {
_, _ = w.Write([]byte{' '})
}
stringifyValue(w, v.Index(i))
}
_, _ = w.Write([]byte{']'})
return return
case reflect.Struct: case reflect.Struct:
if v.Type().Name() != "" { stringifyStruct(w, v)
_, _ = w.Write([]byte(v.Type().String()))
}
// special handling of Timestamp values
if v.Type() == timestampType {
fmt.Fprintf(w, "{%s}", v.Interface())
return
}
_, _ = w.Write([]byte{'{'})
var sep bool
for i := 0; i < v.NumField(); i++ {
fv := v.Field(i)
if fv.Kind() == reflect.Ptr && fv.IsNil() {
continue
}
if fv.Kind() == reflect.Slice && fv.IsNil() {
continue
}
if sep {
_, _ = w.Write([]byte(", "))
} else {
sep = true
}
_, _ = w.Write([]byte(v.Type().Field(i).Name))
_, _ = w.Write([]byte{':'})
stringifyValue(w, fv)
}
_, _ = w.Write([]byte{'}'})
default: default:
if v.CanInterface() { if v.CanInterface() {
fmt.Fprint(w, v.Interface()) fmt.Fprint(w, v.Interface())
} }
} }
} }
func stringifySlice(w io.Writer, v reflect.Value) {
_, _ = w.Write([]byte{'['})
for i := 0; i < v.Len(); i++ {
if i > 0 {
_, _ = w.Write([]byte{' '})
}
stringifyValue(w, v.Index(i))
}
_, _ = w.Write([]byte{']'})
}
func stringifyStruct(w io.Writer, v reflect.Value) {
if v.Type().Name() != "" {
_, _ = w.Write([]byte(v.Type().String()))
}
// special handling of Timestamp values
if v.Type() == timestampType {
fmt.Fprintf(w, "{%s}", v.Interface())
return
}
_, _ = w.Write([]byte{'{'})
var sep bool
for i := 0; i < v.NumField(); i++ {
fv := v.Field(i)
if fv.Kind() == reflect.Ptr && fv.IsNil() {
continue
}
if fv.Kind() == reflect.Slice && fv.IsNil() {
continue
}
if sep {
_, _ = w.Write([]byte(", "))
} else {
sep = true
}
_, _ = w.Write([]byte(v.Type().Field(i).Name))
_, _ = w.Write([]byte{':'})
stringifyValue(w, fv)
}
_, _ = w.Write([]byte{'}'})
}

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)
} }