From b41ec45823848d8cc06977989f0e4ea2796c2f14 Mon Sep 17 00:00:00 2001 From: Gianluca Arbezzano Date: Thu, 16 Mar 2017 23:06:05 +0000 Subject: [PATCH] Update digitalocean/godo --- lock.json | 4 +- provider/digitalocean.go | 11 +- .../github.com/digitalocean/godo/.gitignore | 1 + .../github.com/digitalocean/godo/.travis.yml | 3 +- .../github.com/digitalocean/godo/LICENSE.txt | 2 +- vendor/github.com/digitalocean/godo/README.md | 8 +- .../github.com/digitalocean/godo/account.go | 20 +- .../digitalocean/godo/account_test.go | 18 +- vendor/github.com/digitalocean/godo/action.go | 21 +- .../digitalocean/godo/action_test.go | 8 +- .../digitalocean/godo/certificates.go | 120 +++ .../digitalocean/godo/certificates_test.go | 171 ++++ .../github.com/digitalocean/godo/domains.go | 59 +- .../digitalocean/godo/domains_test.go | 24 +- .../digitalocean/godo/droplet_actions.go | 254 ++++-- .../digitalocean/godo/droplet_actions_test.go | 419 ++++++++- .../github.com/digitalocean/godo/droplets.go | 302 +++++-- .../digitalocean/godo/droplets_test.go | 240 ++++-- .../digitalocean/godo/floating_ips.go | 134 +++ .../digitalocean/godo/floating_ips_actions.go | 108 +++ .../godo/floating_ips_actions_test.go | 167 ++++ .../digitalocean/godo/floating_ips_test.go | 149 ++++ vendor/github.com/digitalocean/godo/godo.go | 103 ++- .../github.com/digitalocean/godo/godo_test.go | 148 +++- .../digitalocean/godo/image_actions.go | 48 +- .../digitalocean/godo/image_actions_test.go | 39 +- vendor/github.com/digitalocean/godo/images.go | 67 +- .../digitalocean/godo/images_test.go | 20 +- vendor/github.com/digitalocean/godo/keys.go | 71 +- .../github.com/digitalocean/godo/keys_test.go | 20 +- vendor/github.com/digitalocean/godo/links.go | 11 +- .../digitalocean/godo/load_balancers.go | 275 ++++++ .../digitalocean/godo/load_balancers_test.go | 796 ++++++++++++++++++ .../github.com/digitalocean/godo/regions.go | 12 +- .../digitalocean/godo/regions_test.go | 6 +- vendor/github.com/digitalocean/godo/sizes.go | 8 +- .../digitalocean/godo/sizes_test.go | 6 +- .../github.com/digitalocean/godo/snapshots.go | 139 +++ .../digitalocean/godo/snapshots_test.go | 186 ++++ .../github.com/digitalocean/godo/storage.go | 237 ++++++ .../digitalocean/godo/storage_actions.go | 137 +++ .../digitalocean/godo/storage_actions_test.go | 179 ++++ .../digitalocean/godo/storage_test.go | 436 ++++++++++ .../github.com/digitalocean/godo/strings.go | 99 ++- vendor/github.com/digitalocean/godo/tags.go | 234 +++++ .../github.com/digitalocean/godo/tags_test.go | 384 +++++++++ .../digitalocean/godo/timestamp_test.go | 6 + .../digitalocean/godo/util/droplet.go | 16 +- .../digitalocean/godo/util/droplet_test.go | 4 +- 49 files changed, 5394 insertions(+), 536 deletions(-) create mode 100755 vendor/github.com/digitalocean/godo/.gitignore create mode 100644 vendor/github.com/digitalocean/godo/certificates.go create mode 100644 vendor/github.com/digitalocean/godo/certificates_test.go create mode 100644 vendor/github.com/digitalocean/godo/floating_ips.go create mode 100644 vendor/github.com/digitalocean/godo/floating_ips_actions.go create mode 100644 vendor/github.com/digitalocean/godo/floating_ips_actions_test.go create mode 100644 vendor/github.com/digitalocean/godo/floating_ips_test.go create mode 100644 vendor/github.com/digitalocean/godo/load_balancers.go create mode 100644 vendor/github.com/digitalocean/godo/load_balancers_test.go create mode 100644 vendor/github.com/digitalocean/godo/snapshots.go create mode 100644 vendor/github.com/digitalocean/godo/snapshots_test.go create mode 100644 vendor/github.com/digitalocean/godo/storage.go create mode 100644 vendor/github.com/digitalocean/godo/storage_actions.go create mode 100644 vendor/github.com/digitalocean/godo/storage_actions_test.go create mode 100644 vendor/github.com/digitalocean/godo/storage_test.go create mode 100644 vendor/github.com/digitalocean/godo/tags.go create mode 100644 vendor/github.com/digitalocean/godo/tags_test.go diff --git a/lock.json b/lock.json index 28a9041..bf35396 100644 --- a/lock.json +++ b/lock.json @@ -35,8 +35,8 @@ }, { "name": "github.com/digitalocean/godo", - "version": "v0.9.0", - "revision": "2a0d64a42bb60a95677748a4d5729af6184330b4", + "version": "v1.0.0", + "revision": "84099941ba2381607e1b05ffd4822781af86675e", "packages": [ "." ] diff --git a/provider/digitalocean.go b/provider/digitalocean.go index c9b4e65..f331366 100644 --- a/provider/digitalocean.go +++ b/provider/digitalocean.go @@ -1,6 +1,7 @@ package provider import ( + "context" "fmt" "strconv" "strings" @@ -16,6 +17,7 @@ import ( type DigitalOceanProvider struct { client *godo.Client config map[string]string + ctx context.Context } func NewDigitalOceanProvider(c map[string]string) (autoscaler.Provider, error) { @@ -27,6 +29,7 @@ func NewDigitalOceanProvider(c map[string]string) (autoscaler.Provider, error) { p := DigitalOceanProvider{ client: client, config: c, + ctx: context.Background(), } return p, nil } @@ -52,7 +55,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool Slug: p.config["image"], }, } - droplet, _, err := p.client.Droplets.Create(createRequest) + droplet, _, err := p.client.Droplets.Create(p.ctx, createRequest) responseChannel <- response{ err: err, droplet: droplet, @@ -62,7 +65,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool } } else { // 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, PerPage: 500, }) @@ -80,7 +83,7 @@ func (p DigitalOceanProvider) Scale(serviceId string, target int, direction bool if p.isGoodToBeDeleted(single, serviceId) && ii < target { go func() { defer wg.Done() - _, err := p.client.Droplets.Delete(single.ID) + _, err := p.client.Droplets.Delete(p.ctx, single.ID) responseChannel <- response{ err: err, 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 { 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 - actions, _, _ := p.client.Droplets.Actions(droplet.ID, &godo.ListOptions{ + actions, _, _ := p.client.Droplets.Actions(p.ctx, droplet.ID, &godo.ListOptions{ Page: 1, PerPage: 500, }) diff --git a/vendor/github.com/digitalocean/godo/.gitignore b/vendor/github.com/digitalocean/godo/.gitignore new file mode 100755 index 0000000..48b8bf9 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/vendor/github.com/digitalocean/godo/.travis.yml b/vendor/github.com/digitalocean/godo/.travis.yml index 245a2f5..f918f8d 100644 --- a/vendor/github.com/digitalocean/godo/.travis.yml +++ b/vendor/github.com/digitalocean/godo/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.3 - - 1.4 + - 1.7 - tip diff --git a/vendor/github.com/digitalocean/godo/LICENSE.txt b/vendor/github.com/digitalocean/godo/LICENSE.txt index d9cd60c..43c5d2e 100644 --- a/vendor/github.com/digitalocean/godo/LICENSE.txt +++ b/vendor/github.com/digitalocean/godo/LICENSE.txt @@ -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 diff --git a/vendor/github.com/digitalocean/godo/README.md b/vendor/github.com/digitalocean/godo/README.md index 03d0c07..4d5cdf8 100644 --- a/vendor/github.com/digitalocean/godo/README.md +++ b/vendor/github.com/digitalocean/godo/README.md @@ -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 { 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: ```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 list := []godo.Droplet{} // create options. initially, these will be blank opt := &godo.ListOptions{} for { - droplets, resp, err := client.Droplets.List(opt) + droplets, resp, err := client.Droplets.List(ctx, opt) if err != nil { return nil, err } diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go index 7623c8c..18eed97 100644 --- a/vendor/github.com/digitalocean/godo/account.go +++ b/vendor/github.com/digitalocean/godo/account.go @@ -1,10 +1,12 @@ package godo +import "context" + // AccountService is an interface for interfacing with the Account // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2/#account type AccountService interface { - Get() (*Account, *Response, error) + Get(context.Context) (*Account, *Response, error) } // AccountServiceOp handles communication with the Account related methods of @@ -17,10 +19,13 @@ var _ AccountService = &AccountServiceOp{} // Account represents a DigitalOcean Account type Account struct { - DropletLimit int `json:"droplet_limit,omitempty"` - Email string `json:"email,omitempty"` - UUID string `json:"uuid,omitempty"` - EmailVerified bool `json:"email_verified,omitempty"` + DropletLimit int `json:"droplet_limit,omitempty"` + FloatingIPLimit int `json:"floating_ip_limit,omitempty"` + Email string `json:"email,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 { @@ -32,10 +37,11 @@ func (r Account) String() string { } // Get DigitalOcean account info -func (s *AccountServiceOp) Get() (*Account, *Response, error) { +func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) { + path := "v2/account" - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/digitalocean/godo/account_test.go b/vendor/github.com/digitalocean/godo/account_test.go index 5d0a8a6..0d0e38c 100644 --- a/vendor/github.com/digitalocean/godo/account_test.go +++ b/vendor/github.com/digitalocean/godo/account_test.go @@ -17,6 +17,7 @@ func TestAccountGet(t *testing.T) { response := ` { "account": { "droplet_limit": 25, + "floating_ip_limit": 25, "email": "sammy@digitalocean.com", "uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", "email_verified": true @@ -26,12 +27,12 @@ func TestAccountGet(t *testing.T) { fmt.Fprint(w, response) }) - acct, _, err := client.Account.Get() + acct, _, err := client.Account.Get(ctx) if err != nil { 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} if !reflect.DeepEqual(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) { acct := &Account{ - DropletLimit: 25, - Email: "sammy@digitalocean.com", - UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", - EmailVerified: true, + DropletLimit: 25, + FloatingIPLimit: 25, + Email: "sammy@digitalocean.com", + UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", + EmailVerified: true, + Status: "active", + StatusMessage: "message", } 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 { t.Errorf("Account.String returned %+v, expected %+v", stringified, expected) } diff --git a/vendor/github.com/digitalocean/godo/action.go b/vendor/github.com/digitalocean/godo/action.go index e19493b..9baef21 100644 --- a/vendor/github.com/digitalocean/godo/action.go +++ b/vendor/github.com/digitalocean/godo/action.go @@ -1,6 +1,9 @@ package godo -import "fmt" +import ( + "context" + "fmt" +) const ( actionsBasePath = "v2/actions" @@ -15,8 +18,8 @@ const ( // ActionsService handles communction with action related methods of the // DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions type ActionsService interface { - List(*ListOptions) ([]Action, *Response, error) - Get(int) (*Action, *Response, error) + List(context.Context, *ListOptions) ([]Action, *Response, error) + Get(context.Context, int) (*Action, *Response, error) } // ActionsServiceOp handles communition with the image action related methods of the @@ -33,7 +36,7 @@ type actionsRoot struct { } type actionRoot struct { - Event Action `json:"action"` + Event *Action `json:"action"` } // Action represents a DigitalOcean Action @@ -50,14 +53,14 @@ type Action struct { } // 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, err := addOptions(path, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -75,13 +78,13 @@ func (s *ActionsServiceOp) List(opt *ListOptions) ([]Action, *Response, error) { } // 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 { return nil, nil, NewArgError("id", "cannot be less than 1") } 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 { return nil, nil, err } @@ -92,7 +95,7 @@ func (s *ActionsServiceOp) Get(id int) (*Action, *Response, error) { return nil, resp, err } - return &root.Event, resp, err + return root.Event, resp, err } func (a Action) String() string { diff --git a/vendor/github.com/digitalocean/godo/action_test.go b/vendor/github.com/digitalocean/godo/action_test.go index 9c63da0..f3a2e3c 100644 --- a/vendor/github.com/digitalocean/godo/action_test.go +++ b/vendor/github.com/digitalocean/godo/action_test.go @@ -17,7 +17,7 @@ func TestAction_List(t *testing.T) { testMethod(t, r, "GET") }) - actions, _, err := client.Actions.List(nil) + actions, _, err := client.Actions.List(ctx, nil) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -37,7 +37,7 @@ func TestAction_ListActionMultiplePages(t *testing.T) { testMethod(t, r, "GET") }) - _, resp, err := client.Actions.List(nil) + _, resp, err := client.Actions.List(ctx, nil) if err != nil { t.Fatal(nil) } @@ -68,7 +68,7 @@ func TestAction_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Actions.List(opt) + _, resp, err := client.Actions.List(ctx, opt) if err != nil { t.Fatal(err) } @@ -85,7 +85,7 @@ func TestAction_Get(t *testing.T) { testMethod(t, r, "GET") }) - action, _, err := client.Actions.Get(12345) + action, _, err := client.Actions.Get(ctx, 12345) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/vendor/github.com/digitalocean/godo/certificates.go b/vendor/github.com/digitalocean/godo/certificates.go new file mode 100644 index 0000000..b48e80e --- /dev/null +++ b/vendor/github.com/digitalocean/godo/certificates.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/certificates_test.go b/vendor/github.com/digitalocean/godo/certificates_test.go new file mode 100644 index 0000000..14c794e --- /dev/null +++ b/vendor/github.com/digitalocean/godo/certificates_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/domains.go b/vendor/github.com/digitalocean/godo/domains.go index 31de178..fdc2a84 100644 --- a/vendor/github.com/digitalocean/godo/domains.go +++ b/vendor/github.com/digitalocean/godo/domains.go @@ -1,6 +1,9 @@ package godo -import "fmt" +import ( + "context" + "fmt" +) const domainsBasePath = "v2/domains" @@ -8,16 +11,16 @@ const domainsBasePath = "v2/domains" // See: https://developers.digitalocean.com/documentation/v2#domains and // https://developers.digitalocean.com/documentation/v2#domain-records type DomainsService interface { - List(*ListOptions) ([]Domain, *Response, error) - Get(string) (*Domain, *Response, error) - Create(*DomainCreateRequest) (*Domain, *Response, error) - Delete(string) (*Response, error) + List(context.Context, *ListOptions) ([]Domain, *Response, error) + Get(context.Context, string) (*Domain, *Response, error) + Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error) + Delete(context.Context, string) (*Response, error) - Records(string, *ListOptions) ([]DomainRecord, *Response, error) - Record(string, int) (*DomainRecord, *Response, error) - DeleteRecord(string, int) (*Response, error) - EditRecord(string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error) - CreateRecord(string, *DomainRecordEditRequest) (*DomainRecord, *Response, error) + Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error) + Record(context.Context, string, int) (*DomainRecord, *Response, error) + DeleteRecord(context.Context, string, int) (*Response, error) + EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error) + CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error) } // DomainsServiceOp handles communication with the domain related methods of the @@ -88,14 +91,14 @@ func (d Domain) String() string { } // 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, err := addOptions(path, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { 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. -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 { return nil, nil, NewArgError("name", "cannot be an empty string") } 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 { return nil, nil, err } @@ -135,14 +138,14 @@ func (s *DomainsServiceOp) Get(name string) (*Domain, *Response, error) { } // 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 { return nil, nil, NewArgError("createRequest", "cannot be nil") } path := domainsBasePath - req, err := s.client.NewRequest("POST", path, createRequest) + req, err := s.client.NewRequest(ctx, "POST", path, createRequest) if err != nil { return nil, nil, err } @@ -156,14 +159,14 @@ func (s *DomainsServiceOp) Create(createRequest *DomainCreateRequest) (*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 { return nil, NewArgError("name", "cannot be an empty string") } 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 { return nil, err } @@ -184,7 +187,7 @@ func (d DomainRecordEditRequest) String() string { } // 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 { 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 } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { 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 -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 { 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) - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { 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 -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 { 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) - req, err := s.client.NewRequest("DELETE", path, nil) + req, err := s.client.NewRequest(ctx, "DELETE", path, nil) if err != nil { return nil, err } @@ -261,7 +264,7 @@ func (s *DomainsServiceOp) DeleteRecord(domain string, id int) (*Response, error } // EditRecord edits a record using a DomainRecordEditRequest -func (s *DomainsServiceOp) EditRecord( +func (s *DomainsServiceOp) EditRecord(ctx context.Context, domain string, id int, editRequest *DomainRecordEditRequest, @@ -280,7 +283,7 @@ func (s *DomainsServiceOp) EditRecord( 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 { return nil, nil, err } @@ -295,7 +298,7 @@ func (s *DomainsServiceOp) EditRecord( } // CreateRecord creates a record using a DomainRecordEditRequest -func (s *DomainsServiceOp) CreateRecord( +func (s *DomainsServiceOp) CreateRecord(ctx context.Context, domain string, createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) { if len(domain) < 1 { @@ -307,7 +310,7 @@ func (s *DomainsServiceOp) CreateRecord( } 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 { return nil, nil, err diff --git a/vendor/github.com/digitalocean/godo/domains_test.go b/vendor/github.com/digitalocean/godo/domains_test.go index 6dbd724..851b303 100644 --- a/vendor/github.com/digitalocean/godo/domains_test.go +++ b/vendor/github.com/digitalocean/godo/domains_test.go @@ -17,7 +17,7 @@ func TestDomains_ListDomains(t *testing.T) { 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 { 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"}}}`) }) - _, resp, err := client.Domains.List(nil) + _, resp, err := client.Domains.List(ctx, nil) if err != nil { t.Fatal(err) } @@ -68,7 +68,7 @@ func TestDomains_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Domains.List(opt) + _, resp, err := client.Domains.List(ctx, opt) if err != nil { t.Fatal(err) } @@ -85,7 +85,7 @@ func TestDomains_GetDomain(t *testing.T) { 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 { 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"}}`) }) - domain, _, err := client.Domains.Create(createRequest) + domain, _, err := client.Domains.Create(ctx, createRequest) if err != nil { t.Errorf("Domains.Create returned error: %v", err) } @@ -139,7 +139,7 @@ func TestDomains_Destroy(t *testing.T) { testMethod(t, r, "DELETE") }) - _, err := client.Domains.Delete("example.com") + _, err := client.Domains.Delete(ctx, "example.com") if err != nil { 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}]}`) }) - records, _, err := client.Domains.Records("example.com", nil) + records, _, err := client.Domains.Records(ctx, "example.com", nil) if err != nil { t.Errorf("Domains.List returned error: %v", err) } @@ -179,7 +179,7 @@ func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) { }) dro := &ListOptions{PerPage: 2} - records, _, err := client.Domains.Records("example.com", dro) + records, _, err := client.Domains.Records(ctx, "example.com", dro) if err != nil { 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}}`) }) - record, _, err := client.Domains.Record("example.com", 1) + record, _, err := client.Domains.Record(ctx, "example.com", 1) if err != nil { t.Errorf("Domains.GetRecord returned error: %v", err) } @@ -218,7 +218,7 @@ func TestDomains_DeleteRecordForDomainName(t *testing.T) { testMethod(t, r, "DELETE") }) - _, err := client.Domains.DeleteRecord("example.com", 1) + _, err := client.Domains.DeleteRecord(ctx, "example.com", 1) if err != nil { 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}}`) }) - record, _, err := client.Domains.CreateRecord("example.com", createRequest) + record, _, err := client.Domains.CreateRecord(ctx, "example.com", createRequest) if err != nil { t.Errorf("Domains.CreateRecord returned error: %v", err) } @@ -293,7 +293,7 @@ func TestDomains_EditRecordForDomainName(t *testing.T) { 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 { t.Errorf("Domains.EditRecord returned error: %v", err) } diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go index 7c9c3fb..cd8b94e 100644 --- a/vendor/github.com/digitalocean/godo/droplet_actions.go +++ b/vendor/github.com/digitalocean/godo/droplet_actions.go @@ -1,6 +1,7 @@ package godo import ( + "context" "fmt" "net/url" ) @@ -8,32 +9,42 @@ import ( // ActionRequest reprents DigitalOcean Action Request 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 // See: https://developers.digitalocean.com/documentation/v2#droplet-actions type DropletActionsService interface { - Shutdown(int) (*Action, *Response, error) - PowerOff(int) (*Action, *Response, error) - PowerOn(int) (*Action, *Response, error) - PowerCycle(int) (*Action, *Response, error) - Reboot(int) (*Action, *Response, error) - Restore(int, int) (*Action, *Response, error) - Resize(int, string, bool) (*Action, *Response, error) - Rename(int, string) (*Action, *Response, error) - Snapshot(int, string) (*Action, *Response, error) - DisableBackups(int) (*Action, *Response, error) - PasswordReset(int) (*Action, *Response, error) - RebuildByImageID(int, int) (*Action, *Response, error) - RebuildByImageSlug(int, string) (*Action, *Response, error) - ChangeKernel(int, int) (*Action, *Response, error) - EnableIPv6(int) (*Action, *Response, error) - EnablePrivateNetworking(int) (*Action, *Response, error) - Upgrade(int) (*Action, *Response, error) - Get(int, int) (*Action, *Response, error) - GetByURI(string) (*Action, *Response, error) + Shutdown(context.Context, int) (*Action, *Response, error) + ShutdownByTag(context.Context, string) (*Action, *Response, error) + PowerOff(context.Context, int) (*Action, *Response, error) + PowerOffByTag(context.Context, string) (*Action, *Response, error) + PowerOn(context.Context, int) (*Action, *Response, error) + PowerOnByTag(context.Context, string) (*Action, *Response, error) + PowerCycle(context.Context, int) (*Action, *Response, error) + PowerCycleByTag(context.Context, string) (*Action, *Response, error) + Reboot(context.Context, int) (*Action, *Response, error) + Restore(context.Context, int, int) (*Action, *Response, error) + Resize(context.Context, int, string, bool) (*Action, *Response, error) + Rename(context.Context, int, string) (*Action, *Response, error) + Snapshot(context.Context, int, string) (*Action, *Response, error) + SnapshotByTag(context.Context, string, string) (*Action, *Response, error) + EnableBackups(context.Context, int) (*Action, *Response, error) + EnableBackupsByTag(context.Context, string) (*Action, *Response, error) + DisableBackups(context.Context, int) (*Action, *Response, error) + DisableBackupsByTag(context.Context, 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. type DropletActionsServiceOp struct { client *Client @@ -42,125 +53,189 @@ type DropletActionsServiceOp struct { var _ DropletActionsService = &DropletActionsServiceOp{} // 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"} - 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 -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"} - 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 -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"} - 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 -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"} - 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 -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"} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } // 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" request := &ActionRequest{ "type": requestType, "image": float64(imageID), } - return s.doAction(id, request) + return s.doAction(ctx, id, request) } // 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" request := &ActionRequest{ "type": requestType, "size": sizeSlug, "disk": resizeDisk, } - return s.doAction(id, request) + return s.doAction(ctx, id, request) } // 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" request := &ActionRequest{ "type": requestType, "name": name, } - return s.doAction(id, request) + return s.doAction(ctx, id, request) } // 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" request := &ActionRequest{ "type": requestType, "name": name, } - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// DisableBackups disables backups for a droplet. -func (s *DropletActionsServiceOp) DisableBackups(id int) (*Action, *Response, error) { +// SnapshotByTag snapshots Droplets matched by a Tag. +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"} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// PasswordReset resets the password for a droplet. -func (s *DropletActionsServiceOp) PasswordReset(id int) (*Action, *Response, error) { +// DisableBackupsByTag disables backups for Droplet matched by a Tag. +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"} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// RebuildByImageID rebuilds a droplet droplet from an image with a given id. -func (s *DropletActionsServiceOp) RebuildByImageID(id, imageID int) (*Action, *Response, error) { +// RebuildByImageID rebuilds a Droplet from an image with a given id. +func (s *DropletActionsServiceOp) RebuildByImageID(ctx context.Context, id, imageID int) (*Action, *Response, error) { 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. -func (s *DropletActionsServiceOp) RebuildByImageSlug(id int, slug string) (*Action, *Response, error) { +// RebuildByImageSlug rebuilds a Droplet from an Image matched by a given Slug. +func (s *DropletActionsServiceOp) RebuildByImageSlug(ctx context.Context, id int, slug string) (*Action, *Response, error) { request := &ActionRequest{"type": "rebuild", "image": slug} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// ChangeKernel changes the kernel for a droplet. -func (s *DropletActionsServiceOp) ChangeKernel(id, kernelID int) (*Action, *Response, error) { +// ChangeKernel changes the kernel for a Droplet. +func (s *DropletActionsServiceOp) ChangeKernel(ctx context.Context, id, kernelID int) (*Action, *Response, error) { request := &ActionRequest{"type": "change_kernel", "kernel": kernelID} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// EnableIPv6 enables IPv6 for a droplet. -func (s *DropletActionsServiceOp) EnableIPv6(id int) (*Action, *Response, error) { +// EnableIPv6 enables IPv6 for a Droplet. +func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Action, *Response, error) { request := &ActionRequest{"type": "enable_ipv6"} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// EnablePrivateNetworking enables private networking for a droplet. -func (s *DropletActionsServiceOp) EnablePrivateNetworking(id int) (*Action, *Response, error) { +// EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag. +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"} - return s.doAction(id, request) + return s.doAction(ctx, id, request) } -// Upgrade a droplet. -func (s *DropletActionsServiceOp) Upgrade(id int) (*Action, *Response, error) { +// EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag. +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"} - 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 { 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) - req, err := s.client.NewRequest("POST", path, request) + req, err := s.client.NewRequest(ctx, "POST", path, request) if err != nil { return nil, nil, err } @@ -182,11 +257,36 @@ func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Act 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) Get(dropletID, actionID int) (*Action, *Response, error) { +func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) (*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 { 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) - return s.get(path) + return s.get(ctx, path) } -// GetByURI gets an action for a particular droplet by id. -func (s *DropletActionsServiceOp) GetByURI(rawurl string) (*Action, *Response, error) { +// GetByURI gets an action for a particular Droplet by id. +func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) (*Action, *Response, error) { u, err := url.Parse(rawurl) if err != nil { return nil, nil, err } - return s.get(u.Path) + return s.get(ctx, u.Path) } -func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) { - req, err := s.client.NewRequest("GET", path, nil) +func (s *DropletActionsServiceOp) 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 } @@ -222,10 +322,14 @@ func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) { return nil, resp, err } - return &root.Event, resp, err + return root.Event, resp, err } func dropletActionPath(dropletID int) string { return fmt.Sprintf("v2/droplets/%d/actions", dropletID) } + +func dropletActionPathByTag(tag string) string { + return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag) +} diff --git a/vendor/github.com/digitalocean/godo/droplet_actions_test.go b/vendor/github.com/digitalocean/godo/droplet_actions_test.go index 2e25a94..d48f260 100644 --- a/vendor/github.com/digitalocean/godo/droplet_actions_test.go +++ b/vendor/github.com/digitalocean/godo/droplet_actions_test.go @@ -31,7 +31,7 @@ func TestDropletActions_Shutdown(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.Shutdown(1) + action, _, err := client.DropletActions.Shutdown(ctx, 1) if err != nil { 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) { setup() defer teardown() @@ -65,7 +103,7 @@ func TestDropletAction_PowerOff(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.PowerOff(1) + action, _, err := client.DropletActions.PowerOff(ctx, 1) if err != nil { 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) { setup() defer teardown() @@ -99,7 +175,7 @@ func TestDropletAction_PowerOn(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.PowerOn(1) + action, _, err := client.DropletActions.PowerOn(ctx, 1) if err != nil { 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) { setup() 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 { 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 { 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 { 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"}}`) }) - action, _, err := client.DropletActions.Rename(1, "Droplet-Name") + action, _, err := client.DropletActions.Rename(ctx, 1, "Droplet-Name") if err != nil { 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 { 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) { setup() defer teardown() @@ -316,7 +468,7 @@ func TestDropletAction_Snapshot(t *testing.T) { 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 { 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) { setup() defer teardown() @@ -351,7 +617,7 @@ func TestDropletAction_DisableBackups(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.DisableBackups(1) + action, _, err := client.DropletActions.DisableBackups(ctx, 1) if err != nil { 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) { setup() defer teardown() @@ -386,7 +691,7 @@ func TestDropletAction_PasswordReset(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.PasswordReset(1) + action, _, err := client.DropletActions.PasswordReset(ctx, 1) if err != nil { 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"}}`) }) - action, _, err := client.DropletActions.RebuildByImageID(1, 2) + action, _, err := client.DropletActions.RebuildByImageID(ctx, 1, 2) if err != nil { 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"}}`) }) - action, _, err := client.DropletActions.RebuildByImageSlug(1, "Image-Name") + action, _, err := client.DropletActions.RebuildByImageSlug(ctx, 1, "Image-Name") if err != nil { 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"}}`) }) - action, _, err := client.DropletActions.ChangeKernel(1, 2) + action, _, err := client.DropletActions.ChangeKernel(ctx, 1, 2) if err != nil { 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"}}`) }) - action, _, err := client.DropletActions.EnableIPv6(1) + action, _, err := client.DropletActions.EnableIPv6(ctx, 1) if err != nil { 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) { setup() defer teardown() @@ -564,7 +908,7 @@ func TestDropletAction_EnablePrivateNetworking(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.EnablePrivateNetworking(1) + action, _, err := client.DropletActions.EnablePrivateNetworking(ctx, 1) if err != nil { 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) { setup() defer teardown() @@ -599,7 +982,7 @@ func TestDropletAction_Upgrade(t *testing.T) { fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) - action, _, err := client.DropletActions.Upgrade(1) + action, _, err := client.DropletActions.Upgrade(ctx, 1) if err != nil { 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"}}`) }) - action, _, err := client.DropletActions.Get(123, 456) + action, _, err := client.DropletActions.Get(ctx, 123, 456) if err != nil { t.Errorf("DropletActions.Get returned error: %v", err) } diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go index 696d621..b145d98 100644 --- a/vendor/github.com/digitalocean/godo/droplets.go +++ b/vendor/github.com/digitalocean/godo/droplets.go @@ -1,28 +1,35 @@ package godo import ( + "context" "encoding/json" + "errors" "fmt" ) 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 // See: https://developers.digitalocean.com/documentation/v2#droplets type DropletsService interface { - List(*ListOptions) ([]Droplet, *Response, error) - Get(int) (*Droplet, *Response, error) - Create(*DropletCreateRequest) (*Droplet, *Response, error) - Delete(int) (*Response, error) - Kernels(int, *ListOptions) ([]Kernel, *Response, error) - Snapshots(int, *ListOptions) ([]Image, *Response, error) - Backups(int, *ListOptions) ([]Image, *Response, error) - Actions(int, *ListOptions) ([]Action, *Response, error) - Neighbors(int) ([]Droplet, *Response, error) + List(context.Context, *ListOptions) ([]Droplet, *Response, error) + ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error) + Get(context.Context, int) (*Droplet, *Response, error) + Create(context.Context, *DropletCreateRequest) (*Droplet, *Response, error) + CreateMultiple(context.Context, *DropletMultiCreateRequest) ([]Droplet, *Response, error) + Delete(context.Context, int) (*Response, error) + DeleteByTag(context.Context, string) (*Response, error) + Kernels(context.Context, int, *ListOptions) ([]Kernel, *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. type DropletsServiceOp struct { client *Client @@ -32,22 +39,71 @@ var _ DropletsService = &DropletsServiceOp{} // Droplet represents a DigitalOcean Droplet type Droplet struct { - ID int `json:"id,float64,omitempty"` - Name string `json:"name,omitempty"` - Memory int `json:"memory,omitempty"` - Vcpus int `json:"vcpus,omitempty"` - Disk int `json:"disk,omitempty"` - Region *Region `json:"region,omitempty"` - Image *Image `json:"image,omitempty"` - Size *Size `json:"size,omitempty"` - SizeSlug string `json:"size_slug,omitempty"` - BackupIDs []int `json:"backup_ids,omitempty"` - SnapshotIDs []int `json:"snapshot_ids,omitempty"` - Locked bool `json:"locked,bool,omitempty"` - Status string `json:"status,omitempty"` - Networks *Networks `json:"networks,omitempty"` - ActionIDs []int `json:"action_ids,omitempty"` - Created string `json:"created_at,omitempty"` + ID int `json:"id,float64,omitempty"` + Name string `json:"name,omitempty"` + Memory int `json:"memory,omitempty"` + Vcpus int `json:"vcpus,omitempty"` + Disk int `json:"disk,omitempty"` + Region *Region `json:"region,omitempty"` + Image *Image `json:"image,omitempty"` + Size *Size `json:"size,omitempty"` + SizeSlug string `json:"size_slug,omitempty"` + BackupIDs []int `json:"backup_ids,omitempty"` + NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"` + SnapshotIDs []int `json:"snapshot_ids,omitempty"` + Features []string `json:"features,omitempty"` + Locked bool `json:"locked,bool,omitempty"` + Status string `json:"status,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 @@ -57,6 +113,12 @@ type Kernel struct { 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 func (d Droplet) String() string { return Stringify(d) @@ -78,7 +140,7 @@ type kernelsRoot struct { Links *Links `json:"links"` } -type snapshotsRoot struct { +type dropletSnapshotsRoot struct { Snapshots []Image `json:"snapshots,omitempty"` Links *Links `json:"links"` } @@ -104,6 +166,27 @@ func (d DropletCreateImage) MarshalJSON() ([]byte, error) { 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. type DropletCreateSSHKey struct { ID int @@ -120,7 +203,7 @@ func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) { return json.Marshal(d.ID) } -// DropletCreateRequest represents a request to create a droplet. +// DropletCreateRequest represents a request to create a Droplet. type DropletCreateRequest struct { Name string `json:"name"` Region string `json:"region"` @@ -130,20 +213,42 @@ type DropletCreateRequest struct { Backups bool `json:"backups"` IPv6 bool `json:"ipv6"` PrivateNetworking bool `json:"private_networking"` + Monitoring bool `json:"monitoring"` 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 { 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 { V4 []NetworkV4 `json:"v4,omitempty"` V6 []NetworkV6 `json:"v6,omitempty"` } -// NetworkV4 represents a DigitalOcean IPv4 Network +// NetworkV4 represents a DigitalOcean IPv4 Network. type NetworkV4 struct { IPAddress string `json:"ip_address,omitempty"` Netmask string `json:"netmask,omitempty"` @@ -167,15 +272,9 @@ func (n NetworkV6) String() string { return Stringify(n) } -// List all droplets -func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) { - path := dropletBasePath - path, err := addOptions(path, opt) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest("GET", path, nil) +// Performs a list request given a path. +func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) { + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -192,15 +291,37 @@ func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) return root.Droplets, resp, err } -// Get individual droplet -func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) { +// List all Droplets. +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 { return nil, nil, NewArgError("dropletID", "cannot be less than 1") } 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 { return nil, nil, err } @@ -214,15 +335,15 @@ func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) { return root.Droplet, resp, err } -// Create droplet -func (s *DropletsServiceOp) Create(createRequest *DropletCreateRequest) (*Droplet, *Response, error) { +// Create Droplet +func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCreateRequest) (*Droplet, *Response, error) { if createRequest == nil { return nil, nil, NewArgError("createRequest", "cannot be nil") } path := dropletBasePath - req, err := s.client.NewRequest("POST", path, createRequest) + req, err := s.client.NewRequest(ctx, "POST", path, createRequest) if err != nil { return nil, nil, err } @@ -239,15 +360,34 @@ func (s *DropletsServiceOp) Create(createRequest *DropletCreateRequest) (*Drople return root.Droplet, resp, err } -// Delete droplet -func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) { - if dropletID < 1 { - return nil, NewArgError("dropletID", "cannot be less than 1") +// CreateMultiple creates multiple Droplets. +func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) { + if createRequest == nil { + 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 { return nil, err } @@ -257,8 +397,30 @@ func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) { return resp, err } -// Kernels lists kernels available for a droplet. -func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel, *Response, error) { +// Delete Droplet. +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 { 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 } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -283,8 +445,8 @@ func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel, return root.Kernels, resp, err } -// Actions lists the actions for a droplet. -func (s *DropletsServiceOp) Actions(dropletID int, opt *ListOptions) ([]Action, *Response, error) { +// Actions lists the actions for a Droplet. +func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *ListOptions) ([]Action, *Response, error) { if dropletID < 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 } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -312,8 +474,8 @@ func (s *DropletsServiceOp) Actions(dropletID int, opt *ListOptions) ([]Action, return root.Actions, resp, err } -// Backups lists the backups for a droplet. -func (s *DropletsServiceOp) Backups(dropletID int, opt *ListOptions) ([]Image, *Response, error) { +// Backups lists the backups for a Droplet. +func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) { if dropletID < 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 } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -341,8 +503,8 @@ func (s *DropletsServiceOp) Backups(dropletID int, opt *ListOptions) ([]Image, * return root.Backups, resp, err } -// Snapshots lists the snapshots available for a droplet. -func (s *DropletsServiceOp) Snapshots(dropletID int, opt *ListOptions) ([]Image, *Response, error) { +// Snapshots lists the snapshots available for a Droplet. +func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) { if dropletID < 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 } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } - root := new(snapshotsRoot) + root := new(dropletSnapshotsRoot) resp, err := s.client.Do(req, root) if err != nil { return nil, resp, err @@ -370,15 +532,15 @@ func (s *DropletsServiceOp) Snapshots(dropletID int, opt *ListOptions) ([]Image, return root.Snapshots, resp, err } -// Neighbors lists the neighbors for a droplet. -func (s *DropletsServiceOp) Neighbors(dropletID int) ([]Droplet, *Response, error) { +// Neighbors lists the neighbors for a Droplet. +func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Droplet, *Response, error) { if dropletID < 1 { return nil, nil, NewArgError("dropletID", "cannot be less than 1") } 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 { return nil, nil, err } @@ -392,8 +554,8 @@ func (s *DropletsServiceOp) Neighbors(dropletID int) ([]Droplet, *Response, erro return root.Droplets, resp, err } -func (s *DropletsServiceOp) dropletActionStatus(uri string) (string, error) { - action, _, err := s.client.DropletActions.GetByURI(uri) +func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) { + action, _, err := s.client.DropletActions.GetByURI(ctx, uri) if err != nil { return "", err diff --git a/vendor/github.com/digitalocean/godo/droplets_test.go b/vendor/github.com/digitalocean/godo/droplets_test.go index c853ff4..1784a8b 100644 --- a/vendor/github.com/digitalocean/godo/droplets_test.go +++ b/vendor/github.com/digitalocean/godo/droplets_test.go @@ -17,14 +17,38 @@ func TestDroplets_ListDroplets(t *testing.T) { fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) }) - droplets, _, err := client.Droplets.List(nil) + droplets, _, err := client.Droplets.List(ctx, nil) if err != nil { t.Errorf("Droplets.List returned error: %v", err) } expected := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(droplets, expected) { - t.Errorf("Droplets.List returned %+v, expected %+v", droplets, expected) + 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)) }) - _, resp, err := client.Droplets.List(nil) + _, resp, err := client.Droplets.List(ctx, nil) if err != nil { t.Fatal(err) } @@ -84,7 +108,7 @@ func TestDroplets_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Droplets.List(opt) + _, resp, err := client.Droplets.List(ctx, opt) if err != nil { t.Fatal(err) } @@ -101,14 +125,14 @@ func TestDroplets_GetDroplet(t *testing.T) { fmt.Fprint(w, `{"droplet":{"id":12345}}`) }) - droplets, _, err := client.Droplets.Get(12345) + droplets, _, err := client.Droplets.Get(ctx, 12345) if err != nil { t.Errorf("Droplet.Get returned error: %v", err) } expected := &Droplet{ID: 12345} 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{ 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) { @@ -135,6 +165,68 @@ func TestDroplets_Create(t *testing.T) { "backups": false, "ipv6": 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{} @@ -147,15 +239,19 @@ func TestDroplets_Create(t *testing.T) { 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 { - 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) } @@ -172,7 +268,25 @@ func TestDroplets_Destroy(t *testing.T) { 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 { t.Errorf("Droplet.Delete returned error: %v", err) } @@ -188,14 +302,14 @@ func TestDroplets_Kernels(t *testing.T) { }) opt := &ListOptions{Page: 2} - kernels, _, err := client.Droplets.Kernels(12345, opt) + kernels, _, err := client.Droplets.Kernels(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Kernels returned error: %v", err) } expected := []Kernel{{ID: 1}, {ID: 2}} 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} - snapshots, _, err := client.Droplets.Snapshots(12345, opt) + snapshots, _, err := client.Droplets.Snapshots(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Snapshots returned error: %v", err) } expected := []Image{{ID: 1}, {ID: 2}} 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} - backups, _, err := client.Droplets.Backups(12345, opt) + backups, _, err := client.Droplets.Backups(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Backups returned error: %v", err) } expected := []Image{{ID: 1}, {ID: 2}} 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} - actions, _, err := client.Droplets.Actions(12345, opt) + actions, _, err := client.Droplets.Actions(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Actions returned error: %v", err) } expected := []Action{{ID: 1}, {ID: 2}} 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}]}`) }) - neighbors, _, err := client.Droplets.Neighbors(12345) + neighbors, _, err := client.Droplets.Neighbors(ctx, 12345) if err != nil { t.Errorf("Droplets.Neighbors returned error: %v", err) } expected := []Droplet{{ID: 1}, {ID: 2}} 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() expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}` 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() 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 { - 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{ - Slug: "region", - Name: "Region", - Sizes: []string{"1", "2"}, - Available: true, + ipv6 := "1000:1000:1000:1000:0000:0000:004D:B001" + + d.Networks = &Networks{ + V4: []NetworkV4{ + {IPAddress: "192.168.0.1", Type: "public"}, + {IPAddress: "10.0.0.1", Type: "private"}, + }, + V6: []NetworkV6{ + {IPAddress: ipv6, Type: "public"}, + }, } - image := &Image{ - ID: 1, - Name: "Image", - Type: "snapshot", - Distribution: "Ubuntu", - Slug: "image", - Public: true, - Regions: []string{"one", "two"}, - MinDiskSize: 20, - Created: "2013-11-27T09:24:55Z", + ip, err := d.PublicIPv4() + if err != nil { + t.Errorf("unknown error") } - size := &Size{ - Slug: "size", - 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}, + if got, expected := ip, "192.168.0.1"; got != expected { + t.Errorf("Droplet.PublicIPv4 returned %s; expected %s", got, expected) } - droplet := &Droplet{ - ID: 1, - Name: "droplet", - Memory: 123, - Vcpus: 456, - Disk: 789, - Region: region, - Image: image, - Size: size, - BackupIDs: []int{1}, - SnapshotIDs: []int{1}, - ActionIDs: []int{1}, - Locked: false, - Status: "active", - Networks: networks, - SizeSlug: "1gb", + ip, err = d.PrivateIPv4() + if err != nil { + t.Errorf("unknown error") } - stringified := droplet.String() - expected := `godo.Droplet{ID:1, Name:"droplet", Memory:123, Vcpus:456, Disk:789, Region:godo.Region{Slug:"region", Name:"Region", Sizes:["1" "2"], Available:true}, Image:godo.Image{ID:1, Name:"Image", 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:""}` - if expected != stringified { - t.Errorf("Droplet.String returned %+v, expected %+v", stringified, expected) + if got, expected := ip, "10.0.0.1"; got != expected { + t.Errorf("Droplet.PrivateIPv4 returned %s; expected %s", got, 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) } } diff --git a/vendor/github.com/digitalocean/godo/floating_ips.go b/vendor/github.com/digitalocean/godo/floating_ips.go new file mode 100644 index 0000000..13e9832 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/floating_ips.go @@ -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 +} diff --git a/vendor/github.com/digitalocean/godo/floating_ips_actions.go b/vendor/github.com/digitalocean/godo/floating_ips_actions.go new file mode 100644 index 0000000..5afa051 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/floating_ips_actions.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/floating_ips_actions_test.go b/vendor/github.com/digitalocean/godo/floating_ips_actions_test.go new file mode 100644 index 0000000..a6a7afc --- /dev/null +++ b/vendor/github.com/digitalocean/godo/floating_ips_actions_test.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/floating_ips_test.go b/vendor/github.com/digitalocean/godo/floating_ips_test.go new file mode 100644 index 0000000..5c29e80 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/floating_ips_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index c5052a5..0f9d7a9 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -2,6 +2,7 @@ package godo import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -17,14 +18,14 @@ import ( ) const ( - libraryVersion = "0.1.0" + libraryVersion = "1.0.0" defaultBaseURL = "https://api.digitalocean.com/" userAgent = "godo/" + libraryVersion mediaType = "application/json" - headerRateLimit = "X-RateLimit-Limit" - headerRateRemaining = "X-RateLimit-Remaining" - headerRateReset = "X-RateLimit-Reset" + headerRateLimit = "RateLimit-Limit" + headerRateRemaining = "RateLimit-Remaining" + headerRateReset = "RateLimit-Reset" ) // Client manages communication with DigitalOcean V2 API. @@ -43,16 +44,24 @@ type Client struct { Rate Rate // Services used for communicating with the API - Account AccountService - Actions ActionsService - Domains DomainsService - Droplets DropletsService - DropletActions DropletActionsService - Images ImagesService - ImageActions ImageActionsService - Keys KeysService - Regions RegionsService - Sizes SizesService + Account AccountService + Actions ActionsService + Domains DomainsService + Droplets DropletsService + DropletActions DropletActionsService + Images ImagesService + ImageActions ImageActionsService + Keys KeysService + Regions RegionsService + 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 onRequestCompleted RequestCompletionCallback @@ -91,7 +100,10 @@ type ErrorResponse struct { Response *http.Response // 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. @@ -102,7 +114,7 @@ type Rate struct { // The number of remaining requests the client can make this hour. Remaining int `json:"remaining"` - // The time at w\hic the current rate limit will reset. + // The time at which the current rate limit will reset. Reset Timestamp `json:"reset"` } @@ -147,19 +159,63 @@ func NewClient(httpClient *http.Client) *Client { c.Domains = &DomainsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c} + c.FloatingIPs = &FloatingIPsServiceOp{client: c} + c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} c.Images = &ImagesServiceOp{client: c} c.ImageActions = &ImageActionsServiceOp{client: c} c.Keys = &KeysServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c} + c.Snapshots = &SnapshotsServiceOp{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 } +// ClientOpt are options for New. +type ClientOpt func(*Client) error + +// New returns a new DIgitalOcean API client instance. +func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) { + c := NewClient(httpClient) + for _, opt := range opts { + if err := opt(c); err != nil { + return nil, err + } + } + + return c, nil +} + +// SetBaseURL is a client option for setting the base URL. +func SetBaseURL(bu string) ClientOpt { + return func(c *Client) error { + u, err := url.Parse(bu) + if err != nil { + return err + } + + c.BaseURL = u + return nil + } +} + +// SetUserAgent is a client option for setting the user agent. +func SetUserAgent(ua string) ClientOpt { + return func(c *Client) error { + c.UserAgent = fmt.Sprintf("%s+%s", ua, c.UserAgent) + return nil + } +} + // NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the // BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the // value pointed to by body is JSON encoded and included in as the request body. -func (c *Client) NewRequest(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) if err != nil { return nil, err @@ -169,7 +225,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ buf := new(bytes.Buffer) if body != nil { - err := json.NewEncoder(buf).Encode(body) + err = json.NewEncoder(buf).Encode(body) if err != nil { return nil, err } @@ -180,9 +236,10 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return nil, err } + req = req.WithContext(ctx) req.Header.Add("Content-Type", mediaType) req.Header.Add("Accept", mediaType) - req.Header.Add("User-Agent", userAgent) + req.Header.Add("User-Agent", c.UserAgent) return req, nil } @@ -261,12 +318,12 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { if v != nil { if w, ok := v.(io.Writer); ok { - _, err := io.Copy(w, resp.Body) + _, err = io.Copy(w, resp.Body) if err != nil { return nil, err } } else { - err := json.NewDecoder(resp.Body).Decode(v) + err = json.NewDecoder(resp.Body).Decode(v) if err != nil { return nil, err } @@ -276,6 +333,10 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { return response, err } func (r *ErrorResponse) Error() string { + if r.RequestID != "" { + return fmt.Sprintf("%v %v: %d (request %q) %v", + r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message) + } return fmt.Sprintf("%v %v: %d %v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message) } diff --git a/vendor/github.com/digitalocean/godo/godo_test.go b/vendor/github.com/digitalocean/godo/godo_test.go index 5da13f9..aafc51e 100644 --- a/vendor/github.com/digitalocean/godo/godo_test.go +++ b/vendor/github.com/digitalocean/godo/godo_test.go @@ -1,7 +1,7 @@ package godo import ( - "encoding/json" + "context" "fmt" "io/ioutil" "net/http" @@ -17,6 +17,8 @@ import ( var ( mux *http.ServeMux + ctx = context.TODO() + client *Client server *httptest.Server @@ -68,17 +70,65 @@ func testURLParseError(t *testing.T, err error) { } } -func TestNewClient(t *testing.T) { - c := NewClient(nil) - if c.BaseURL.String() != defaultBaseURL { - t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL.String(), defaultBaseURL) +func testClientServices(t *testing.T, c *Client) { + services := []string{ + "Account", + "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 { 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) { c := NewClient(nil) @@ -86,8 +136,8 @@ func TestNewRequest(t *testing.T) { inBody, outBody := &DropletCreateRequest{Name: "l"}, `{"name":"l","region":"","size":"","image":0,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+ - `"private_networking":false}`+"\n" - req, _ := c.NewRequest("GET", inURL, inBody) + `"private_networking":false,"monitoring":false,"tags":null}`+"\n" + req, _ := c.NewRequest(ctx, "GET", inURL, inBody) // test relative URL was expanded if req.URL.String() != outURL { @@ -114,8 +164,8 @@ func TestNewRequest_withUserData(t *testing.T) { inBody, outBody := &DropletCreateRequest{Name: "l", UserData: "u"}, `{"name":"l","region":"","size":"","image":0,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+ - `"private_networking":false,"user_data":"u"}`+"\n" - req, _ := c.NewRequest("GET", inURL, inBody) + `"private_networking":false,"monitoring":false,"user_data":"u","tags":null}`+"\n" + req, _ := c.NewRequest(ctx, "GET", inURL, inBody) // test relative URL was expanded 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) { c := NewClient(nil) - _, err := c.NewRequest("GET", ":", nil) + _, err := c.NewRequest(ctx, "GET", ":", nil) 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) { setup() defer teardown() @@ -172,7 +222,7 @@ func TestDo(t *testing.T) { fmt.Fprint(w, `{"A":"a"}`) }) - req, _ := client.NewRequest("GET", "/", nil) + req, _ := client.NewRequest(ctx, "GET", "/", nil) body := new(foo) _, err := client.Do(req, body) if err != nil { @@ -193,7 +243,7 @@ func TestDo_httpError(t *testing.T) { http.Error(w, "Bad Request", 400) }) - req, _ := client.NewRequest("GET", "/", nil) + req, _ := client.NewRequest(ctx, "GET", "/", nil) _, err := client.Do(req, nil) if err == nil { @@ -211,7 +261,7 @@ func TestDo_redirectLoop(t *testing.T) { http.Redirect(w, r, "/", http.StatusFound) }) - req, _ := client.NewRequest("GET", "/", nil) + req, _ := client.NewRequest(ctx, "GET", "/", nil) _, err := client.Do(req, nil) if err == nil { @@ -296,7 +346,7 @@ func TestDo_rateLimit(t *testing.T) { 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) if err != nil { t.Fatalf("Do(): %v", err) @@ -327,7 +377,7 @@ func TestDo_rateLimit_errorResponse(t *testing.T) { var expected int - req, _ := client.NewRequest("GET", "/", nil) + req, _ := client.NewRequest(ctx, "GET", "/", nil) _, _ = client.Do(req, nil) if expected = 60; client.Rate.Limit != expected { @@ -369,7 +419,7 @@ func TestDo_completion_callback(t *testing.T) { fmt.Fprint(w, `{"A":"a"}`) }) - req, _ := client.NewRequest("GET", "/", nil) + req, _ := client.NewRequest(ctx, "GET", "/", nil) body := new(foo) var completedReq *http.Request 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) +} diff --git a/vendor/github.com/digitalocean/godo/image_actions.go b/vendor/github.com/digitalocean/godo/image_actions.go index 2d05385..c4201c0 100644 --- a/vendor/github.com/digitalocean/godo/image_actions.go +++ b/vendor/github.com/digitalocean/godo/image_actions.go @@ -1,13 +1,17 @@ package godo -import "fmt" +import ( + "context" + "fmt" +) // ImageActionsService is an interface for interfacing with the image actions // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2#image-actions type ImageActionsService interface { - Get(int, int) (*Action, *Response, error) - Transfer(int, *ActionRequest) (*Action, *Response, error) + Get(context.Context, int, int) (*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 @@ -19,7 +23,7 @@ type ImageActionsServiceOp struct { var _ ImageActionsService = &ImageActionsServiceOp{} // 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 { 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) - req, err := i.client.NewRequest("POST", path, transferRequest) + req, err := i.client.NewRequest(ctx, "POST", path, transferRequest) if err != nil { return nil, nil, err } @@ -41,11 +45,37 @@ func (i *ImageActionsServiceOp) Transfer(imageID int, transferRequest *ActionReq 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. -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 { 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) - req, err := i.client.NewRequest("GET", path, nil) + req, err := i.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -67,5 +97,5 @@ func (i *ImageActionsServiceOp) Get(imageID, actionID int) (*Action, *Response, return nil, resp, err } - return &root.Event, resp, err + return root.Event, resp, err } diff --git a/vendor/github.com/digitalocean/godo/image_actions_test.go b/vendor/github.com/digitalocean/godo/image_actions_test.go index 5d3cf67..e9a97af 100644 --- a/vendor/github.com/digitalocean/godo/image_actions_test.go +++ b/vendor/github.com/digitalocean/godo/image_actions_test.go @@ -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 { 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"}}`) }) - action, _, err := client.ImageActions.Get(123, 456) + action, _, err := client.ImageActions.Get(ctx, 123, 456) if err != nil { t.Errorf("ImageActions.Get returned error: %v", err) } diff --git a/vendor/github.com/digitalocean/godo/images.go b/vendor/github.com/digitalocean/godo/images.go index 96894f8..c808af6 100644 --- a/vendor/github.com/digitalocean/godo/images.go +++ b/vendor/github.com/digitalocean/godo/images.go @@ -1,6 +1,9 @@ package godo -import "fmt" +import ( + "context" + "fmt" +) const imageBasePath = "v2/images" @@ -8,14 +11,14 @@ const imageBasePath = "v2/images" // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2#images type ImagesService interface { - List(*ListOptions) ([]Image, *Response, error) - ListDistribution(opt *ListOptions) ([]Image, *Response, error) - ListApplication(opt *ListOptions) ([]Image, *Response, error) - ListUser(opt *ListOptions) ([]Image, *Response, error) - GetByID(int) (*Image, *Response, error) - GetBySlug(string) (*Image, *Response, error) - Update(int, *ImageUpdateRequest) (*Image, *Response, error) - Delete(int) (*Response, error) + List(context.Context, *ListOptions) ([]Image, *Response, error) + ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) + ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) + ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) + GetByID(context.Context, int) (*Image, *Response, error) + GetBySlug(context.Context, string) (*Image, *Response, error) + Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error) + Delete(context.Context, int) (*Response, error) } // ImagesServiceOp handles communication with the image related methods of the @@ -45,7 +48,7 @@ type ImageUpdateRequest struct { } type imageRoot struct { - Image Image + Image *Image } type imagesRoot struct { @@ -63,48 +66,48 @@ func (i Image) String() string { } // List lists all the images available. -func (s *ImagesServiceOp) List(opt *ListOptions) ([]Image, *Response, error) { - return s.list(opt, nil) +func (s *ImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) { + return s.list(ctx, opt, nil) } // 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"} - return s.list(opt, &listOpt) + return s.list(ctx, opt, &listOpt) } // 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"} - return s.list(opt, &listOpt) + return s.list(ctx, opt, &listOpt) } // 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} - return s.list(opt, &listOpt) + return s.list(ctx, opt, &listOpt) } // 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 { 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. -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 { return nil, nil, NewArgError("slug", "cannot be blank") } - return s.get(interface{}(slug)) + return s.get(ctx, interface{}(slug)) } // 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 { 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) - req, err := s.client.NewRequest("PUT", path, updateRequest) + req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest) if err != nil { return nil, nil, err } @@ -125,18 +128,18 @@ func (s *ImagesServiceOp) Update(imageID int, updateRequest *ImageUpdateRequest) return nil, resp, err } - return &root.Image, resp, err + return root.Image, resp, err } // 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 { return nil, NewArgError("imageID", "cannot be less than 1") } 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 { return nil, err } @@ -147,10 +150,10 @@ func (s *ImagesServiceOp) Delete(imageID int) (*Response, error) { } // 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) - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -161,11 +164,11 @@ func (s *ImagesServiceOp) get(ID interface{}) (*Image, *Response, error) { return nil, resp, err } - return &root.Image, resp, err + return root.Image, resp, err } // 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, err := addOptions(path, opt) if err != nil { @@ -176,7 +179,7 @@ func (s *ImagesServiceOp) list(opt *ListOptions, listOpt *listImageOptions) ([]I return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/digitalocean/godo/images_test.go b/vendor/github.com/digitalocean/godo/images_test.go index 3fd4d1a..41f0a84 100644 --- a/vendor/github.com/digitalocean/godo/images_test.go +++ b/vendor/github.com/digitalocean/godo/images_test.go @@ -17,7 +17,7 @@ func TestImages_List(t *testing.T) { fmt.Fprint(w, `{"images":[{"id":1},{"id":2}]}`) }) - images, _, err := client.Images.List(nil) + images, _, err := client.Images.List(ctx, nil) if err != nil { 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}]}`) }) - images, _, err := client.Images.ListDistribution(nil) + images, _, err := client.Images.ListDistribution(ctx, nil) if err != nil { 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}]}`) }) - images, _, err := client.Images.ListApplication(nil) + images, _, err := client.Images.ListApplication(ctx, nil) if err != nil { 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}]}`) }) - images, _, err := client.Images.ListUser(nil) + images, _, err := client.Images.ListUser(ctx, nil) if err != nil { 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"}}}`) }) - _, resp, err := client.Images.List(&ListOptions{Page: 2}) + _, resp, err := client.Images.List(ctx, &ListOptions{Page: 2}) if err != nil { t.Fatal(err) } @@ -143,7 +143,7 @@ func TestImages_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Images.List(opt) + _, resp, err := client.Images.List(ctx, opt) if err != nil { t.Fatal(err) } @@ -160,7 +160,7 @@ func TestImages_GetImageByID(t *testing.T) { fmt.Fprint(w, `{"image":{"id":12345}}`) }) - images, _, err := client.Images.GetByID(12345) + images, _, err := client.Images.GetByID(ctx, 12345) if err != nil { t.Errorf("Image.GetByID returned error: %v", err) } @@ -180,7 +180,7 @@ func TestImages_GetImageBySlug(t *testing.T) { fmt.Fprint(w, `{"image":{"id":12345}}`) }) - images, _, err := client.Images.GetBySlug("ubuntu") + images, _, err := client.Images.GetBySlug(ctx, "ubuntu") if err != nil { t.Errorf("Image.GetBySlug returned error: %v", err) } @@ -217,7 +217,7 @@ func TestImages_Update(t *testing.T) { fmt.Fprintf(w, `{"image":{"id":1}}`) }) - image, _, err := client.Images.Update(12345, updateRequest) + image, _, err := client.Images.Update(ctx, 12345, updateRequest) if err != nil { t.Errorf("Images.Update returned error: %v", err) } else { @@ -235,7 +235,7 @@ func TestImages_Destroy(t *testing.T) { testMethod(t, r, "DELETE") }) - _, err := client.Images.Delete(12345) + _, err := client.Images.Delete(ctx, 12345) if err != nil { t.Errorf("Image.Delete returned error: %v", err) } diff --git a/vendor/github.com/digitalocean/godo/keys.go b/vendor/github.com/digitalocean/godo/keys.go index 8dc50f8..7b336c4 100644 --- a/vendor/github.com/digitalocean/godo/keys.go +++ b/vendor/github.com/digitalocean/godo/keys.go @@ -1,6 +1,9 @@ package godo -import "fmt" +import ( + "context" + "fmt" +) const keysBasePath = "v2/account/keys" @@ -8,14 +11,14 @@ const keysBasePath = "v2/account/keys" // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2#keys type KeysService interface { - List(*ListOptions) ([]Key, *Response, error) - GetByID(int) (*Key, *Response, error) - GetByFingerprint(string) (*Key, *Response, error) - Create(*KeyCreateRequest) (*Key, *Response, error) - UpdateByID(int, *KeyUpdateRequest) (*Key, *Response, error) - UpdateByFingerprint(string, *KeyUpdateRequest) (*Key, *Response, error) - DeleteByID(int) (*Response, error) - DeleteByFingerprint(string) (*Response, error) + List(context.Context, *ListOptions) ([]Key, *Response, error) + GetByID(context.Context, int) (*Key, *Response, error) + GetByFingerprint(context.Context, string) (*Key, *Response, error) + Create(context.Context, *KeyCreateRequest) (*Key, *Response, error) + UpdateByID(context.Context, int, *KeyUpdateRequest) (*Key, *Response, error) + UpdateByFingerprint(context.Context, string, *KeyUpdateRequest) (*Key, *Response, error) + DeleteByID(context.Context, int) (*Response, error) + DeleteByFingerprint(context.Context, string) (*Response, error) } // KeysServiceOp handles communication with key related method of the @@ -45,7 +48,7 @@ type keysRoot struct { } type keyRoot struct { - SSHKey Key `json:"ssh_key"` + SSHKey *Key `json:"ssh_key"` } func (s Key) String() string { @@ -59,14 +62,14 @@ type KeyCreateRequest struct { } // 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, err := addOptions(path, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -84,8 +87,8 @@ func (s *KeysServiceOp) List(opt *ListOptions) ([]Key, *Response, error) { } // Performs a get given a path -func (s *KeysServiceOp) get(path string) (*Key, *Response, error) { - req, err := s.client.NewRequest("GET", path, nil) +func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) { + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } @@ -96,36 +99,36 @@ func (s *KeysServiceOp) get(path string) (*Key, *Response, error) { return nil, resp, err } - return &root.SSHKey, resp, err + return root.SSHKey, resp, err } // 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 { return nil, nil, NewArgError("keyID", "cannot be less than 1") } path := fmt.Sprintf("%s/%d", keysBasePath, keyID) - return s.get(path) + return s.get(ctx, path) } // 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 { return nil, nil, NewArgError("fingerprint", "cannot not be empty") } path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint) - return s.get(path) + return s.get(ctx, path) } // 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 { 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 { return nil, nil, err } @@ -136,11 +139,11 @@ func (s *KeysServiceOp) Create(createRequest *KeyCreateRequest) (*Key, *Response return nil, resp, err } - return &root.SSHKey, resp, err + return root.SSHKey, resp, err } // 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 { 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) - req, err := s.client.NewRequest("PUT", path, updateRequest) + req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest) if err != nil { return nil, nil, err } @@ -161,11 +164,11 @@ func (s *KeysServiceOp) UpdateByID(keyID int, updateRequest *KeyUpdateRequest) ( return nil, resp, err } - return &root.SSHKey, resp, err + return root.SSHKey, resp, err } // 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 { 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) - req, err := s.client.NewRequest("PUT", path, updateRequest) + req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest) if err != nil { return nil, nil, err } @@ -186,12 +189,12 @@ func (s *KeysServiceOp) UpdateByFingerprint(fingerprint string, updateRequest *K return nil, resp, err } - return &root.SSHKey, resp, err + return root.SSHKey, resp, err } // Delete key using a path -func (s *KeysServiceOp) delete(path string) (*Response, error) { - req, err := s.client.NewRequest("DELETE", path, nil) +func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) { + req, err := s.client.NewRequest(ctx, "DELETE", path, nil) if err != nil { return nil, err } @@ -202,21 +205,21 @@ func (s *KeysServiceOp) delete(path string) (*Response, error) { } // 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 { return nil, NewArgError("keyID", "cannot be less than 1") } path := fmt.Sprintf("%s/%d", keysBasePath, keyID) - return s.delete(path) + return s.delete(ctx, path) } // 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 { return nil, NewArgError("fingerprint", "cannot be empty") } path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint) - return s.delete(path) + return s.delete(ctx, path) } diff --git a/vendor/github.com/digitalocean/godo/keys_test.go b/vendor/github.com/digitalocean/godo/keys_test.go index b209f61..7f37f9f 100644 --- a/vendor/github.com/digitalocean/godo/keys_test.go +++ b/vendor/github.com/digitalocean/godo/keys_test.go @@ -17,7 +17,7 @@ func TestKeys_List(t *testing.T) { 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 { 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"}}}`) }) - _, resp, err := client.Keys.List(nil) + _, resp, err := client.Keys.List(ctx, nil) if err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestKeys_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Keys.List(opt) + _, resp, err := client.Keys.List(ctx, opt) if err != nil { t.Fatal(err) } @@ -83,7 +83,7 @@ func TestKeys_GetByID(t *testing.T) { fmt.Fprint(w, `{"ssh_key": {"id":12345}}`) }) - keys, _, err := client.Keys.GetByID(12345) + keys, _, err := client.Keys.GetByID(ctx, 12345) if err != nil { 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"}}`) }) - keys, _, err := client.Keys.GetByFingerprint("aa:bb:cc") + keys, _, err := client.Keys.GetByFingerprint(ctx, "aa:bb:cc") if err != nil { 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}}`) }) - key, _, err := client.Keys.Create(createRequest) + key, _, err := client.Keys.Create(ctx, createRequest) if err != nil { 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}}`) }) - key, _, err := client.Keys.UpdateByID(12345, updateRequest) + key, _, err := client.Keys.UpdateByID(ctx, 12345, updateRequest) if err != nil { t.Errorf("Keys.Update returned error: %v", err) } else { @@ -211,7 +211,7 @@ func TestKeys_UpdateByFingerprint(t *testing.T) { 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 { t.Errorf("Keys.Update returned error: %v", err) } else { @@ -229,7 +229,7 @@ func TestKeys_DestroyByID(t *testing.T) { testMethod(t, r, "DELETE") }) - _, err := client.Keys.DeleteByID(12345) + _, err := client.Keys.DeleteByID(ctx, 12345) if err != nil { t.Errorf("Keys.Delete returned error: %v", err) } @@ -243,7 +243,7 @@ func TestKeys_DestroyByFingerprint(t *testing.T) { testMethod(t, r, "DELETE") }) - _, err := client.Keys.DeleteByFingerprint("aa:bb:cc") + _, err := client.Keys.DeleteByFingerprint(ctx, "aa:bb:cc") if err != nil { t.Errorf("Keys.Delete returned error: %v", err) } diff --git a/vendor/github.com/digitalocean/godo/links.go b/vendor/github.com/digitalocean/godo/links.go index 4635105..2098441 100644 --- a/vendor/github.com/digitalocean/godo/links.go +++ b/vendor/github.com/digitalocean/godo/links.go @@ -1,6 +1,7 @@ package godo import ( + "context" "net/url" "strconv" ) @@ -58,11 +59,7 @@ func (l *Links) IsLastPage() bool { } func (p *Pages) isLast() bool { - if p.Last == "" { - return true - } - - return false + return p.Last == "" } func pageForURL(urlText string) (int, error) { @@ -81,6 +78,6 @@ func pageForURL(urlText string) (int, error) { } // Get a link action by id. -func (la *LinkAction) Get(client *Client) (*Action, *Response, error) { - return client.Actions.Get(la.ID) +func (la *LinkAction) Get(ctx context.Context, client *Client) (*Action, *Response, error) { + return client.Actions.Get(ctx, la.ID) } diff --git a/vendor/github.com/digitalocean/godo/load_balancers.go b/vendor/github.com/digitalocean/godo/load_balancers.go new file mode 100644 index 0000000..48b9c26 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/load_balancers.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/load_balancers_test.go b/vendor/github.com/digitalocean/godo/load_balancers_test.go new file mode 100644 index 0000000..898b6a9 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/load_balancers_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/regions.go b/vendor/github.com/digitalocean/godo/regions.go index e44e8bc..ccfe029 100644 --- a/vendor/github.com/digitalocean/godo/regions.go +++ b/vendor/github.com/digitalocean/godo/regions.go @@ -1,10 +1,12 @@ package godo +import "context" + // RegionsService is an interface for interfacing with the regions // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2#regions 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 @@ -29,23 +31,19 @@ type regionsRoot struct { Links *Links `json:"links"` } -type regionRoot struct { - Region Region -} - func (r Region) String() string { return Stringify(r) } // 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, err := addOptions(path, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/digitalocean/godo/regions_test.go b/vendor/github.com/digitalocean/godo/regions_test.go index 3c72576..379d8d4 100644 --- a/vendor/github.com/digitalocean/godo/regions_test.go +++ b/vendor/github.com/digitalocean/godo/regions_test.go @@ -16,7 +16,7 @@ func TestRegions_List(t *testing.T) { fmt.Fprint(w, `{"regions":[{"slug":"1"},{"slug":"2"}]}`) }) - regions, _, err := client.Regions.List(nil) + regions, _, err := client.Regions.List(ctx, nil) if err != nil { 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"}}}`) }) - _, resp, err := client.Regions.List(nil) + _, resp, err := client.Regions.List(ctx, nil) if err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestRegions_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Regions.List(opt) + _, resp, err := client.Regions.List(ctx, opt) if err != nil { t.Fatal(err) } diff --git a/vendor/github.com/digitalocean/godo/sizes.go b/vendor/github.com/digitalocean/godo/sizes.go index 7f454e7..b695792 100644 --- a/vendor/github.com/digitalocean/godo/sizes.go +++ b/vendor/github.com/digitalocean/godo/sizes.go @@ -1,10 +1,12 @@ package godo +import "context" + // SizesService is an interface for interfacing with the size // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2#sizes 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 @@ -38,14 +40,14 @@ type sizesRoot struct { } // 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, err := addOptions(path, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", path, nil) + req, err := s.client.NewRequest(ctx, "GET", path, nil) if err != nil { return nil, nil, err } diff --git a/vendor/github.com/digitalocean/godo/sizes_test.go b/vendor/github.com/digitalocean/godo/sizes_test.go index 390ddf4..1b96a77 100644 --- a/vendor/github.com/digitalocean/godo/sizes_test.go +++ b/vendor/github.com/digitalocean/godo/sizes_test.go @@ -16,7 +16,7 @@ func TestSizes_List(t *testing.T) { fmt.Fprint(w, `{"sizes":[{"slug":"1"},{"slug":"2"}]}`) }) - sizes, _, err := client.Sizes.List(nil) + sizes, _, err := client.Sizes.List(ctx, nil) if err != nil { 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"}}}`) }) - _, resp, err := client.Sizes.List(nil) + _, resp, err := client.Sizes.List(ctx, nil) if err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestSizes_RetrievePageByNumber(t *testing.T) { }) opt := &ListOptions{Page: 2} - _, resp, err := client.Sizes.List(opt) + _, resp, err := client.Sizes.List(ctx, opt) if err != nil { t.Fatal(err) } diff --git a/vendor/github.com/digitalocean/godo/snapshots.go b/vendor/github.com/digitalocean/godo/snapshots.go new file mode 100644 index 0000000..5a9e5c5 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/snapshots.go @@ -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 +} diff --git a/vendor/github.com/digitalocean/godo/snapshots_test.go b/vendor/github.com/digitalocean/godo/snapshots_test.go new file mode 100644 index 0000000..e6b8d72 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/snapshots_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/storage.go b/vendor/github.com/digitalocean/godo/storage.go new file mode 100644 index 0000000..fe5d749 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage.go @@ -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®ion=%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) +} diff --git a/vendor/github.com/digitalocean/godo/storage_actions.go b/vendor/github.com/digitalocean/godo/storage_actions.go new file mode 100644 index 0000000..7988868 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage_actions.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/storage_actions_test.go b/vendor/github.com/digitalocean/godo/storage_actions_test.go new file mode 100644 index 0000000..8a06c45 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage_actions_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/storage_test.go b/vendor/github.com/digitalocean/godo/storage_test.go new file mode 100644 index 0000000..350c10b --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/strings.go b/vendor/github.com/digitalocean/godo/strings.go index bbdbc92..4a8bfb6 100644 --- a/vendor/github.com/digitalocean/godo/strings.go +++ b/vendor/github.com/digitalocean/godo/strings.go @@ -3,6 +3,7 @@ package godo import ( "bytes" "fmt" + "io" "reflect" ) @@ -17,7 +18,7 @@ func Stringify(message interface{}) string { } // 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() { _, _ = w.Write([]byte("")) return @@ -29,55 +30,63 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) { case reflect.String: fmt.Fprintf(w, `"%s"`, v) case reflect.Slice: - _, _ = w.Write([]byte{'['}) - for i := 0; i < v.Len(); i++ { - if i > 0 { - _, _ = w.Write([]byte{' '}) - } - - stringifyValue(w, v.Index(i)) - } - - _, _ = w.Write([]byte{']'}) + stringifySlice(w, v) return case reflect.Struct: - if v.Type().Name() != "" { - _, _ = w.Write([]byte(v.Type().String())) - } - - // special handling of Timestamp values - if v.Type() == timestampType { - fmt.Fprintf(w, "{%s}", v.Interface()) - return - } - - _, _ = w.Write([]byte{'{'}) - - var sep bool - for i := 0; i < v.NumField(); i++ { - fv := v.Field(i) - if fv.Kind() == reflect.Ptr && fv.IsNil() { - continue - } - if fv.Kind() == reflect.Slice && fv.IsNil() { - continue - } - - if sep { - _, _ = w.Write([]byte(", ")) - } else { - sep = true - } - - _, _ = w.Write([]byte(v.Type().Field(i).Name)) - _, _ = w.Write([]byte{':'}) - stringifyValue(w, fv) - } - - _, _ = w.Write([]byte{'}'}) + stringifyStruct(w, v) default: if v.CanInterface() { 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{'}'}) +} diff --git a/vendor/github.com/digitalocean/godo/tags.go b/vendor/github.com/digitalocean/godo/tags.go new file mode 100644 index 0000000..709e96c --- /dev/null +++ b/vendor/github.com/digitalocean/godo/tags.go @@ -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 +} diff --git a/vendor/github.com/digitalocean/godo/tags_test.go b/vendor/github.com/digitalocean/godo/tags_test.go new file mode 100644 index 0000000..0ced656 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/tags_test.go @@ -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) + } +} diff --git a/vendor/github.com/digitalocean/godo/timestamp_test.go b/vendor/github.com/digitalocean/godo/timestamp_test.go index 087e8aa..aadaa21 100644 --- a/vendor/github.com/digitalocean/godo/timestamp_test.go +++ b/vendor/github.com/digitalocean/godo/timestamp_test.go @@ -88,6 +88,9 @@ func TestTimstamp_MarshalReflexivity(t *testing.T) { } var got Timestamp err = json.Unmarshal(data, &got) + if err != nil { + t.Errorf("%s: Unmarshal err=%v", data, err) + } if !got.Equal(tc.data) { t.Errorf("%s: %+v != %+v", tc.desc, got, data) } @@ -169,6 +172,9 @@ func TestWrappedTimestamp_MarshalReflexivity(t *testing.T) { } var got WrappedTimestamp err = json.Unmarshal(bytes, &got) + if err != nil { + t.Errorf("%s: Unmarshal err=%v", bytes, err) + } if !got.Time.Equal(tc.data.Time) { t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data) } diff --git a/vendor/github.com/digitalocean/godo/util/droplet.go b/vendor/github.com/digitalocean/godo/util/droplet.go index 4a016bc..e7c48e2 100644 --- a/vendor/github.com/digitalocean/godo/util/droplet.go +++ b/vendor/github.com/digitalocean/godo/util/droplet.go @@ -1,6 +1,7 @@ package util import ( + "context" "fmt" "time" @@ -15,7 +16,7 @@ const ( ) // 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 { return fmt.Errorf("create had no monitor uri") } @@ -23,9 +24,14 @@ func WaitForActive(client *godo.Client, monitorURI string) error { completed := false failCount := 0 for !completed { - action, _, err := client.DropletActions.GetByURI(monitorURI) + action, _, err := client.DropletActions.GetByURI(ctx, monitorURI) if err != nil { + select { + case <-ctx.Done(): + return err + default: + } if failCount <= activeFailure { failCount++ continue @@ -35,7 +41,11 @@ func WaitForActive(client *godo.Client, monitorURI string) error { switch action.Status { case godo.ActionInProgress: - time.Sleep(5 * time.Second) + select { + case <-time.After(5 * time.Second): + case <-ctx.Done(): + return err + } case godo.ActionCompleted: completed = true default: diff --git a/vendor/github.com/digitalocean/godo/util/droplet_test.go b/vendor/github.com/digitalocean/godo/util/droplet_test.go index e0a82ad..fdbca4e 100644 --- a/vendor/github.com/digitalocean/godo/util/droplet_test.go +++ b/vendor/github.com/digitalocean/godo/util/droplet_test.go @@ -1,6 +1,8 @@ package util import ( + "context" + "golang.org/x/oauth2" "github.com/digitalocean/godo" @@ -19,7 +21,7 @@ func ExampleWaitForActive() { uri := "https://api.digitalocean.com/v2/actions/xxxxxxxx" // block until until the action is complete - err := WaitForActive(client, uri) + err := WaitForActive(context.TODO(), client, uri) if err != nil { panic(err) }