diff --git a/README.md b/README.md
index d7fcaaf80..8e032ce6a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Currently supported DNS providers:
- Active Directory
- BIND
- CloudFlare
+ - Digitalocean
- DNSimple
- Gandi
- Google
diff --git a/docs/_providers/digitalocean.md b/docs/_providers/digitalocean.md
new file mode 100644
index 000000000..c28dd1127
--- /dev/null
+++ b/docs/_providers/digitalocean.md
@@ -0,0 +1,40 @@
+---
+name: Digitalocean
+layout: default
+jsId: DIGITALOCEAN
+---
+# Digitalocean Provider
+
+## Configuration
+
+In your providers config json file you must provide your
+[Digitalocean OAuth Token](https://cloud.digitalocean.com/settings/applications)
+
+{% highlight json %}
+{
+ "digitalocean":{
+ "token": "your-digitalocean-ouath-token"
+ }
+}
+{% endhighlight %}
+
+## Metadata
+
+This provider does not recognize any special metadata fields unique to route 53.
+
+## Usage
+
+Example javascript:
+
+{% highlight js %}
+var REG_NAMECOM = NewRegistrar("name.com","NAMEDOTCOM");
+var DO = NewDnsProvider("do", "DIGITALOCEAN");
+
+D("example.tld", REG_NAMECOM, DnsProvider(DO),
+ A("test","1.2.3.4")
+);
+{%endhighlight%}
+
+## Activation
+
+[Create OAuth Token](https://cloud.digitalocean.com/settings/applications)
diff --git a/docs/provider-list.md b/docs/provider-list.md
index d088f951b..418a89855 100644
--- a/docs/provider-list.md
+++ b/docs/provider-list.md
@@ -65,7 +65,6 @@ code to support this provider, please re-open the issue. We'd be glad to help in
AWS R53 (DNS works. Request is to add Registrar support) (#68)
Azure (#42)
ClouDNS (#114)
- Digital Ocean (#125)
Dyn (#61)
Gandi (DNS works. Request is to add Registrar support) (#87)
GoDaddy (#145)
diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go
index 9623a391c..4710d2f18 100755
--- a/integrationTest/integration_test.go
+++ b/integrationTest/integration_test.go
@@ -110,6 +110,9 @@ func runTests(t *testing.T, prv providers.DNSServiceProvider, domainName string,
for _, r := range tst.Records {
rc := models.RecordConfig(*r)
rc.NameFQDN = dnsutil.AddOrigin(rc.Name, domainName)
+ if rc.Target == "**current-domain**" {
+ rc.Target = domainName + "."
+ }
dom.Records = append(dom.Records, &rc)
}
dom2, _ := dom.Copy()
@@ -290,11 +293,13 @@ var tests = []*TestCase{
tc("Change it", cname("foo", "google2.com.")),
tc("Change to A record", a("foo", "1.2.3.4")),
tc("Change back to CNAME", cname("foo", "google.com.")),
+ tc("Record pointing to @", cname("foo", "**current-domain**")),
//NS
tc("Empty"),
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
+ tc("Record pointing to @", ns("foo", "**current-domain**")),
//IDNAs
tc("Empty"),
@@ -311,6 +316,7 @@ var tests = []*TestCase{
tc("Delete one", mx("@", 5, "foo2.com."), mx("@", 15, "foo3.com.")),
tc("Change to other name", mx("@", 5, "foo2.com."), mx("mail", 15, "foo3.com.")),
tc("Change Preference", mx("@", 7, "foo2.com."), mx("mail", 15, "foo3.com.")),
+ tc("Record pointing to @", mx("foo", 8, "**current-domain**")),
//PTR
tc("Empty").IfHasCapability(providers.CanUsePTR),
diff --git a/integrationTest/providers.json b/integrationTest/providers.json
index b28ea1148..4b37a693b 100644
--- a/integrationTest/providers.json
+++ b/integrationTest/providers.json
@@ -12,6 +12,10 @@
"apiuser": "$CF_USER",
"domain": "$CF_DOMAIN"
},
+ "DIGITALOCEAN": {
+ "token": "$DO_TOKEN",
+ "domain": "$DO_DOMAIN"
+ },
"DNSIMPLE": {
"COMMENT": "16/17: no ns records managable. Not even for subdomains.",
"baseurl": "https://api.sandbox.dnsimple.com",
diff --git a/providers/_all/all.go b/providers/_all/all.go
index e1b5a0175..5c13d2a27 100644
--- a/providers/_all/all.go
+++ b/providers/_all/all.go
@@ -6,6 +6,7 @@ import (
_ "github.com/StackExchange/dnscontrol/providers/activedir"
_ "github.com/StackExchange/dnscontrol/providers/bind"
_ "github.com/StackExchange/dnscontrol/providers/cloudflare"
+ _ "github.com/StackExchange/dnscontrol/providers/digitalocean"
_ "github.com/StackExchange/dnscontrol/providers/dnsimple"
_ "github.com/StackExchange/dnscontrol/providers/gandi"
_ "github.com/StackExchange/dnscontrol/providers/google"
diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go
new file mode 100644
index 000000000..ba9f67221
--- /dev/null
+++ b/providers/digitalocean/digitaloceanProvider.go
@@ -0,0 +1,223 @@
+package digitalocean
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/StackExchange/dnscontrol/models"
+ "github.com/StackExchange/dnscontrol/providers"
+ "github.com/StackExchange/dnscontrol/providers/diff"
+ "github.com/miekg/dns/dnsutil"
+
+ "github.com/digitalocean/godo"
+ "golang.org/x/oauth2"
+)
+
+/*
+
+Digitalocean API DNS provider:
+
+Info required in `creds.json`:
+ - token
+
+*/
+
+type DoApi struct {
+ client *godo.Client
+}
+
+var defaultNameServerNames = []string{
+ "ns1.digitalocean.com",
+ "ns2.digitalocean.com",
+ "ns3.digitalocean.com",
+}
+
+func newDo(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
+ if m["token"] == "" {
+ return nil, fmt.Errorf("Digitalocean Token must be provided.")
+ }
+
+ ctx := context.Background()
+ oauthClient := oauth2.NewClient(
+ ctx,
+ oauth2.StaticTokenSource(&oauth2.Token{AccessToken: m["token"]}),
+ )
+ client := godo.NewClient(oauthClient)
+
+ api := &DoApi{client: client}
+
+ // Get a domain to validate the token
+ _, resp, err := api.client.Domains.List(ctx, &godo.ListOptions{PerPage: 1})
+ if err != nil {
+ return nil, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Digitalocean Token is not valid.")
+ }
+
+ return api, nil
+}
+
+func init() {
+ providers.RegisterDomainServiceProviderType("DIGITALOCEAN", newDo, providers.CanUseSRV)
+}
+
+func (api *DoApi) EnsureDomainExists(domain string) error {
+ ctx := context.Background()
+ _, resp, err := api.client.Domains.Get(ctx, domain)
+ if resp.StatusCode == http.StatusNotFound {
+ _, _, err := api.client.Domains.Create(ctx, &godo.DomainCreateRequest{
+ Name: domain,
+ IPAddress: "",
+ })
+ return err
+ } else {
+ return err
+ }
+}
+
+func (api *DoApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
+ return models.StringsToNameservers(defaultNameServerNames), nil
+}
+
+func (api *DoApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
+ ctx := context.Background()
+ dc.Punycode()
+
+ records, err := getRecords(api, dc.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ existingRecords := make([]*models.RecordConfig, len(records))
+ for i := range records {
+ existingRecords[i] = toRc(dc, &records[i])
+ }
+
+ differ := diff.New(dc)
+ _, create, delete, modify := differ.IncrementalDiff(existingRecords)
+
+ var corrections = []*models.Correction{}
+
+ // Deletes first so changing type works etc.
+ for _, m := range delete {
+ id := m.Existing.Original.(*godo.DomainRecord).ID
+ corr := &models.Correction{
+ Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
+ F: func() error {
+ _, err := api.client.Domains.DeleteRecord(ctx, dc.Name, id)
+ return err
+ },
+ }
+ corrections = append(corrections, corr)
+ }
+ for _, m := range create {
+ req := toReq(dc, m.Desired)
+ corr := &models.Correction{
+ Msg: m.String(),
+ F: func() error {
+ _, _, err := api.client.Domains.CreateRecord(ctx, dc.Name, req)
+ return err
+ },
+ }
+ corrections = append(corrections, corr)
+ }
+ for _, m := range modify {
+ id := m.Existing.Original.(*godo.DomainRecord).ID
+ req := toReq(dc, m.Desired)
+ corr := &models.Correction{
+ Msg: fmt.Sprintf("%s, DO ID: %d", m.String(), id),
+ F: func() error {
+ _, _, err := api.client.Domains.EditRecord(ctx, dc.Name, id, req)
+ return err
+ },
+ }
+ corrections = append(corrections, corr)
+ }
+
+ return corrections, nil
+}
+
+func getRecords(api *DoApi, name string) ([]godo.DomainRecord, error) {
+ ctx := context.Background()
+
+ records := []godo.DomainRecord{}
+ opt := &godo.ListOptions{}
+ for {
+ result, resp, err := api.client.Domains.Records(ctx, name, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, d := range result {
+ records = append(records, d)
+ }
+
+ if resp.Links == nil || resp.Links.IsLastPage() {
+ break
+ }
+
+ page, err := resp.Links.CurrentPage()
+ if err != nil {
+ return nil, err
+ }
+
+ opt.Page = page + 1
+ }
+
+ return records, nil
+}
+
+func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
+ // This handles "@" etc.
+ name := dnsutil.AddOrigin(r.Name, dc.Name)
+
+ target := r.Data
+ // Make target FQDN (#rtype_variations)
+ if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" {
+ // If target is the domainname, e.g. cname foo.example.com -> example.com,
+ // DO returns "@" on read even if fqdn was written.
+ if target == "@" {
+ target = dc.Name
+ }
+ target = dnsutil.AddOrigin(target+".", dc.Name)
+ }
+
+ return &models.RecordConfig{
+ NameFQDN: name,
+ Type: r.Type,
+ Target: target,
+ TTL: uint32(r.TTL),
+ MxPreference: uint16(r.Priority),
+ SrvPriority: uint16(r.Priority),
+ SrvWeight: uint16(r.Weight),
+ SrvPort: uint16(r.Port),
+ Original: r,
+ }
+}
+
+func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
+ // DO wants the short name, e.g. @
+ name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
+
+ // DO uses the same property for MX and SRV priority
+ priority := 0
+ switch rc.Type { // #rtype_variations
+ case "MX":
+ priority = int(rc.MxPreference)
+ case "SRV":
+ priority = int(rc.SrvPriority)
+ }
+
+ return &godo.DomainRecordEditRequest{
+ Type: rc.Type,
+ Name: name,
+ Data: rc.Target,
+ TTL: int(rc.TTL),
+ Priority: priority,
+ Port: int(rc.SrvPort),
+ Weight: int(rc.SrvWeight),
+ }
+}
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
new file mode 100644
index 000000000..b89def70b
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -0,0 +1,20 @@
+# Change Log
+
+## [v1.1.0] - 2017-06-06
+
+### Added
+- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola
+- #139 Add TTL field to the Domains. - @xmudrii
+
+### Fixed
+- #143 Fix oauth2.NoContext depreciation. - @jbowens
+- #141 Fix DropletActions on tagged resources. - @xmudrii
+
+## [v1.0.0] - 2017-03-10
+
+### Added
+- #130 Add Convert to ImageActionsService. - @xmudrii
+- #126 Add CertificatesService for managing certificates with the DigitalOcean API. - @viola
+- #125 Add LoadBalancersService for managing load balancers with the DigitalOcean API. - @viola
+- #122 Add GetVolumeByName to StorageService. - @protochron
+- #113 Add context.Context to all calls. - @aybabtme
diff --git a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md
new file mode 100644
index 000000000..f27200a7a
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md
@@ -0,0 +1,23 @@
+# Contributing
+
+If you submit a pull request, please keep the following guidelines in mind:
+
+1. Code should be `go fmt` compliant.
+2. Types, structs and funcs should be documented.
+3. Tests pass.
+
+## Getting set up
+
+Assuming your `$GOPATH` is set up according to your desires, run:
+
+```sh
+go get github.com/digitalocean/godo
+```
+
+## Running tests
+
+When working on code in this repository, tests can be run via:
+
+```sh
+go test .
+```
diff --git a/vendor/github.com/digitalocean/godo/LICENSE.txt b/vendor/github.com/digitalocean/godo/LICENSE.txt
new file mode 100644
index 000000000..43c5d2eee
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/LICENSE.txt
@@ -0,0 +1,55 @@
+Copyright (c) 2014-2016 The godo AUTHORS. All rights reserved.
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+======================
+Portions of the client are based on code at:
+https://github.com/google/go-github/
+
+Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/vendor/github.com/digitalocean/godo/README.md b/vendor/github.com/digitalocean/godo/README.md
new file mode 100644
index 000000000..44b2e4d0b
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/README.md
@@ -0,0 +1,138 @@
+[](https://travis-ci.org/digitalocean/godo)
+
+# Godo
+
+Godo is a Go client library for accessing the DigitalOcean V2 API.
+
+You can view the client API docs here: [http://godoc.org/github.com/digitalocean/godo](http://godoc.org/github.com/digitalocean/godo)
+
+You can view DigitalOcean API docs here: [https://developers.digitalocean.com/documentation/v2/](https://developers.digitalocean.com/documentation/v2/)
+
+
+## Usage
+
+```go
+import "github.com/digitalocean/godo"
+```
+
+Create a new DigitalOcean client, then use the exposed services to
+access different parts of the DigitalOcean API.
+
+### Authentication
+
+Currently, Personal Access Token (PAT) is the only method of
+authenticating with the API. You can manage your tokens
+at the DigitalOcean Control Panel [Applications Page](https://cloud.digitalocean.com/settings/applications).
+
+You can then use your token to create a new client:
+
+```go
+import "golang.org/x/oauth2"
+
+pat := "mytoken"
+type TokenSource struct {
+ AccessToken string
+}
+
+func (t *TokenSource) Token() (*oauth2.Token, error) {
+ token := &oauth2.Token{
+ AccessToken: t.AccessToken,
+ }
+ return token, nil
+}
+
+tokenSource := &TokenSource{
+ AccessToken: pat,
+}
+oauthClient := oauth2.NewClient(context.Background(), tokenSource)
+client := godo.NewClient(oauthClient)
+```
+
+## Examples
+
+
+To create a new Droplet:
+
+```go
+dropletName := "super-cool-droplet"
+
+createRequest := &godo.DropletCreateRequest{
+ Name: dropletName,
+ Region: "nyc3",
+ Size: "512mb",
+ Image: godo.DropletCreateImage{
+ Slug: "ubuntu-14-04-x64",
+ },
+}
+
+ctx := context.TODO()
+
+newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
+
+if err != nil {
+ fmt.Printf("Something bad happened: %s\n\n", err)
+ return err
+}
+```
+
+### Pagination
+
+If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets:
+
+```go
+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(ctx, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ // append the current page's droplets to our list
+ for _, d := range droplets {
+ list = append(list, d)
+ }
+
+ // if we are at the last page, break out the for loop
+ if resp.Links == nil || resp.Links.IsLastPage() {
+ break
+ }
+
+ page, err := resp.Links.CurrentPage()
+ if err != nil {
+ return nil, err
+ }
+
+ // set the page we want for the next request
+ opt.Page = page + 1
+ }
+
+ return list, nil
+}
+```
+
+## Versioning
+
+Each version of the client is tagged and the version is updated accordingly.
+
+Since Go does not have a built-in versioning, a package management tool is
+recommended - a good one that works with git tags is
+[gopkg.in](http://labix.org/gopkg.in).
+
+To see the list of past versions, run `git tag`.
+
+
+## Documentation
+
+For a comprehensive list of examples, check out the [API documentation](https://developers.digitalocean.com/documentation/v2/).
+
+For details on all the functionality in this library, see the [GoDoc](http://godoc.org/github.com/digitalocean/godo) documentation.
+
+
+## Contributing
+
+We love pull requests! Please see the [contribution guidelines](CONTRIBUTING.md).
diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go
new file mode 100644
index 000000000..db25f1002
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/account.go
@@ -0,0 +1,60 @@
+package godo
+
+import (
+ "net/http"
+
+ "github.com/digitalocean/godo/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(context.Context) (*Account, *Response, error)
+}
+
+// AccountServiceOp handles communication with the Account related methods of
+// the DigitalOcean API.
+type AccountServiceOp struct {
+ client *Client
+}
+
+var _ AccountService = &AccountServiceOp{}
+
+// Account represents a DigitalOcean Account
+type Account struct {
+ 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 {
+ Account *Account `json:"account"`
+}
+
+func (r Account) String() string {
+ return Stringify(r)
+}
+
+// Get DigitalOcean account info
+func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) {
+
+ path := "v2/account"
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(accountRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Account, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/action.go b/vendor/github.com/digitalocean/godo/action.go
new file mode 100644
index 000000000..3990f5644
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/action.go
@@ -0,0 +1,105 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const (
+ actionsBasePath = "v2/actions"
+
+ // ActionInProgress is an in progress action status
+ ActionInProgress = "in-progress"
+
+ //ActionCompleted is a completed action status
+ ActionCompleted = "completed"
+)
+
+// ActionsService handles communction with action related methods of the
+// DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions
+type ActionsService interface {
+ 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
+// DigitalOcean API.
+type ActionsServiceOp struct {
+ client *Client
+}
+
+var _ ActionsService = &ActionsServiceOp{}
+
+type actionsRoot struct {
+ Actions []Action `json:"actions"`
+ Links *Links `json:"links"`
+}
+
+type actionRoot struct {
+ Event *Action `json:"action"`
+}
+
+// Action represents a DigitalOcean Action
+type Action struct {
+ ID int `json:"id"`
+ Status string `json:"status"`
+ Type string `json:"type"`
+ StartedAt *Timestamp `json:"started_at"`
+ CompletedAt *Timestamp `json:"completed_at"`
+ ResourceID int `json:"resource_id"`
+ ResourceType string `json:"resource_type"`
+ Region *Region `json:"region,omitempty"`
+ RegionSlug string `json:"region_slug,omitempty"`
+}
+
+// List all actions
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Actions, resp, err
+}
+
+// Get an action by ID.
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Event, resp, err
+}
+
+func (a Action) String() string {
+ return Stringify(a)
+}
diff --git a/vendor/github.com/digitalocean/godo/certificates.go b/vendor/github.com/digitalocean/godo/certificates.go
new file mode 100644
index 000000000..ca95cee75
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/certificates.go
@@ -0,0 +1,122 @@
+package godo
+
+import (
+ "net/http"
+ "path"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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, http.MethodGet, urlStr, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(certificateRoot)
+ resp, err := c.client.Do(ctx, 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, http.MethodGet, urlStr, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(certificatesRoot)
+ resp, err := c.client.Do(ctx, 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, http.MethodPost, certificatesBasePath, cr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(certificateRoot)
+ resp, err := c.client.Do(ctx, 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, http.MethodDelete, urlStr, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return c.client.Do(ctx, req, nil)
+}
diff --git a/vendor/github.com/digitalocean/godo/context/context.go b/vendor/github.com/digitalocean/godo/context/context.go
new file mode 100644
index 000000000..fe49b8dfc
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/context/context.go
@@ -0,0 +1,98 @@
+package context
+
+import "time"
+
+// A Context carries a deadline, a cancelation signal, and other values across
+// API boundaries.
+//
+// Context's methods may be called by multiple goroutines simultaneously.
+type Context interface {
+ // Deadline returns the time when work done on behalf of this context
+ // should be canceled. Deadline returns ok==false when no deadline is
+ // set. Successive calls to Deadline return the same results.
+ Deadline() (deadline time.Time, ok bool)
+
+ // Done returns a channel that's closed when work done on behalf of this
+ // context should be canceled. Done may return nil if this context can
+ // never be canceled. Successive calls to Done return the same value.
+ //
+ // WithCancel arranges for Done to be closed when cancel is called;
+ // WithDeadline arranges for Done to be closed when the deadline
+ // expires; WithTimeout arranges for Done to be closed when the timeout
+ // elapses.
+ //
+ // Done is provided for use in select statements:s
+ //
+ // // Stream generates values with DoSomething and sends them to out
+ // // until DoSomething returns an error or ctx.Done is closed.
+ // func Stream(ctx context.Context, out chan<- Value) error {
+ // for {
+ // v, err := DoSomething(ctx)
+ // if err != nil {
+ // return err
+ // }
+ // select {
+ // case <-ctx.Done():
+ // return ctx.Err()
+ // case out <- v:
+ // }
+ // }
+ // }
+ //
+ // See http://blog.golang.org/pipelines for more examples of how to use
+ // a Done channel for cancelation.
+ Done() <-chan struct{}
+
+ // Err returns a non-nil error value after Done is closed. Err returns
+ // Canceled if the context was canceled or DeadlineExceeded if the
+ // context's deadline passed. No other values for Err are defined.
+ // After Done is closed, successive calls to Err return the same value.
+ Err() error
+
+ // Value returns the value associated with this context for key, or nil
+ // if no value is associated with key. Successive calls to Value with
+ // the same key returns the same result.
+ //
+ // Use context values only for request-scoped data that transits
+ // processes and API boundaries, not for passing optional parameters to
+ // functions.
+ //
+ // A key identifies a specific value in a Context. Functions that wish
+ // to store values in Context typically allocate a key in a global
+ // variable then use that key as the argument to context.WithValue and
+ // Context.Value. A key can be any type that supports equality;
+ // packages should define keys as an unexported type to avoid
+ // collisions.
+ //
+ // Packages that define a Context key should provide type-safe accessors
+ // for the values stores using that key:
+ //
+ // // Package user defines a User type that's stored in Contexts.
+ // package user
+ //
+ // import "golang.org/x/net/context"
+ //
+ // // User is the type of value stored in the Contexts.
+ // type User struct {...}
+ //
+ // // key is an unexported type for keys defined in this package.
+ // // This prevents collisions with keys defined in other packages.
+ // type key int
+ //
+ // // userKey is the key for user.User values in Contexts. It is
+ // // unexported; clients use user.NewContext and user.FromContext
+ // // instead of using this key directly.
+ // var userKey key = 0
+ //
+ // // NewContext returns a new Context that carries value u.
+ // func NewContext(ctx context.Context, u *User) context.Context {
+ // return context.WithValue(ctx, userKey, u)
+ // }
+ //
+ // // FromContext returns the User value stored in ctx, if any.
+ // func FromContext(ctx context.Context) (*User, bool) {
+ // u, ok := ctx.Value(userKey).(*User)
+ // return u, ok
+ // }
+ Value(key interface{}) interface{}
+}
diff --git a/vendor/github.com/digitalocean/godo/context/context_go17.go b/vendor/github.com/digitalocean/godo/context/context_go17.go
new file mode 100644
index 000000000..d5359dedb
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/context/context_go17.go
@@ -0,0 +1,39 @@
+// +build go1.7
+
+package context
+
+import (
+ "context"
+ "net/http"
+)
+
+// DoRequest submits an HTTP request.
+func DoRequest(ctx Context, req *http.Request) (*http.Response, error) {
+ return DoRequestWithClient(ctx, http.DefaultClient, req)
+}
+
+// DoRequestWithClient submits an HTTP request using the specified client.
+func DoRequestWithClient(
+ ctx Context,
+ client *http.Client,
+ req *http.Request) (*http.Response, error) {
+ req = req.WithContext(ctx)
+ return client.Do(req)
+}
+
+// TODO returns a non-nil, empty Context. Code should use context.TODO when
+// it's unclear which Context to use or it is not yet available (because the
+// surrounding function has not yet been extended to accept a Context
+// parameter). TODO is recognized by static analysis tools that determine
+// whether Contexts are propagated correctly in a program.
+func TODO() Context {
+ return context.TODO()
+}
+
+// Background returns a non-nil, empty Context. It is never canceled, has no
+// values, and has no deadline. It is typically used by the main function,
+// initialization, and tests, and as the top-level Context for incoming
+// requests.
+func Background() Context {
+ return context.Background()
+}
diff --git a/vendor/github.com/digitalocean/godo/context/context_pre_go17.go b/vendor/github.com/digitalocean/godo/context/context_pre_go17.go
new file mode 100644
index 000000000..e30adb0cb
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/context/context_pre_go17.go
@@ -0,0 +1,41 @@
+// +build !go1.7
+
+package context
+
+import (
+ "net/http"
+
+ "golang.org/x/net/context"
+ "golang.org/x/net/context/ctxhttp"
+)
+
+// DoRequest submits an HTTP request.
+func DoRequest(ctx Context, req *http.Request) (*http.Response, error) {
+ return DoRequestWithClient(ctx, http.DefaultClient, req)
+}
+
+// DoRequestWithClient submits an HTTP request using the specified client.
+func DoRequestWithClient(
+ ctx Context,
+ client *http.Client,
+ req *http.Request) (*http.Response, error) {
+
+ return ctxhttp.Do(ctx, client, req)
+}
+
+// TODO returns a non-nil, empty Context. Code should use context.TODO when
+// it's unclear which Context to use or it is not yet available (because the
+// surrounding function has not yet been extended to accept a Context
+// parameter). TODO is recognized by static analysis tools that determine
+// whether Contexts are propagated correctly in a program.
+func TODO() Context {
+ return context.TODO()
+}
+
+// Background returns a non-nil, empty Context. It is never canceled, has no
+// values, and has no deadline. It is typically used by the main function,
+// initialization, and tests, and as the top-level Context for incoming
+// requests.
+func Background() Context {
+ return context.Background()
+}
diff --git a/vendor/github.com/digitalocean/godo/doc.go b/vendor/github.com/digitalocean/godo/doc.go
new file mode 100644
index 000000000..e660f794a
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/doc.go
@@ -0,0 +1,2 @@
+// Package godo is the DigtalOcean API v2 client for Go
+package godo
diff --git a/vendor/github.com/digitalocean/godo/domains.go b/vendor/github.com/digitalocean/godo/domains.go
new file mode 100644
index 000000000..091f0d97a
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/domains.go
@@ -0,0 +1,330 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const domainsBasePath = "v2/domains"
+
+// DomainsService is an interface for managing DNS with the DigitalOcean API.
+// See: https://developers.digitalocean.com/documentation/v2#domains and
+// https://developers.digitalocean.com/documentation/v2#domain-records
+type DomainsService interface {
+ 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(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
+// DigitalOcean API.
+type DomainsServiceOp struct {
+ client *Client
+}
+
+var _ DomainsService = &DomainsServiceOp{}
+
+// Domain represents a DigitalOcean domain
+type Domain struct {
+ Name string `json:"name"`
+ TTL int `json:"ttl"`
+ ZoneFile string `json:"zone_file"`
+}
+
+// domainRoot represents a response from the DigitalOcean API
+type domainRoot struct {
+ Domain *Domain `json:"domain"`
+}
+
+type domainsRoot struct {
+ Domains []Domain `json:"domains"`
+ Links *Links `json:"links"`
+}
+
+// DomainCreateRequest respresents a request to create a domain.
+type DomainCreateRequest struct {
+ Name string `json:"name"`
+ IPAddress string `json:"ip_address"`
+}
+
+// DomainRecordRoot is the root of an individual Domain Record response
+type domainRecordRoot struct {
+ DomainRecord *DomainRecord `json:"domain_record"`
+}
+
+// DomainRecordsRoot is the root of a group of Domain Record responses
+type domainRecordsRoot struct {
+ DomainRecords []DomainRecord `json:"domain_records"`
+ Links *Links `json:"links"`
+}
+
+// DomainRecord represents a DigitalOcean DomainRecord
+type DomainRecord struct {
+ ID int `json:"id,float64,omitempty"`
+ Type string `json:"type,omitempty"`
+ Name string `json:"name,omitempty"`
+ Data string `json:"data,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Port int `json:"port,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Weight int `json:"weight,omitempty"`
+}
+
+// DomainRecordEditRequest represents a request to update a domain record.
+type DomainRecordEditRequest struct {
+ Type string `json:"type,omitempty"`
+ Name string `json:"name,omitempty"`
+ Data string `json:"data,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Port int `json:"port,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Weight int `json:"weight,omitempty"`
+}
+
+func (d Domain) String() string {
+ return Stringify(d)
+}
+
+// List all domains.
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(domainsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Domains, resp, err
+}
+
+// Get individual domain. It requires a non-empty domain name.
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(domainRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Domain, resp, err
+}
+
+// Create a new domain
+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(ctx, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(domainRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Domain, resp, err
+}
+
+// Delete domain
+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(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// Converts a DomainRecord to a string.
+func (d DomainRecord) String() string {
+ return Stringify(d)
+}
+
+// Converts a DomainRecordEditRequest to a string.
+func (d DomainRecordEditRequest) String() string {
+ return Stringify(d)
+}
+
+// Records returns a slice of DomainRecords for a domain
+func (s *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")
+ }
+
+ path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
+ path, err := addOptions(path, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(domainRecordsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.DomainRecords, resp, err
+}
+
+// Record returns the record id from a domain
+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")
+ }
+
+ if id < 1 {
+ return nil, nil, NewArgError("id", "cannot be less than 1")
+ }
+
+ path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ record := new(domainRecordRoot)
+ resp, err := s.client.Do(ctx, req, record)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return record.DomainRecord, resp, err
+}
+
+// DeleteRecord deletes a record from a domain identified by id
+func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
+ if len(domain) < 1 {
+ return nil, NewArgError("domain", "cannot be an empty string")
+ }
+
+ if id < 1 {
+ return nil, NewArgError("id", "cannot be less than 1")
+ }
+
+ path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
+
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// EditRecord edits a record using a DomainRecordEditRequest
+func (s *DomainsServiceOp) EditRecord(ctx context.Context,
+ domain string,
+ id int,
+ editRequest *DomainRecordEditRequest,
+) (*DomainRecord, *Response, error) {
+ if len(domain) < 1 {
+ return nil, nil, NewArgError("domain", "cannot be an empty string")
+ }
+
+ if id < 1 {
+ return nil, nil, NewArgError("id", "cannot be less than 1")
+ }
+
+ if editRequest == nil {
+ return nil, nil, NewArgError("editRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
+
+ req, err := s.client.NewRequest(ctx, "PUT", path, editRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ d := new(DomainRecord)
+ resp, err := s.client.Do(ctx, req, d)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return d, resp, err
+}
+
+// CreateRecord creates a record using a DomainRecordEditRequest
+func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
+ domain string,
+ createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
+ if len(domain) < 1 {
+ return nil, nil, NewArgError("domain", "cannot be empty string")
+ }
+
+ if createRequest == nil {
+ return nil, nil, NewArgError("createRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ d := new(domainRecordRoot)
+ resp, err := s.client.Do(ctx, req, d)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return d.DomainRecord, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go
new file mode 100644
index 000000000..a40d19700
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/droplet_actions.go
@@ -0,0 +1,337 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/digitalocean/godo/context"
+)
+
+// ActionRequest reprents DigitalOcean Action Request
+type ActionRequest map[string]interface{}
+
+// 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(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
+// methods of the DigitalOcean API.
+type DropletActionsServiceOp struct {
+ client *Client
+}
+
+var _ DropletActionsService = &DropletActionsServiceOp{}
+
+// Shutdown a Droplet
+func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action, *Response, error) {
+ request := &ActionRequest{"type": "shutdown"}
+ 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(ctx context.Context, id int) (*Action, *Response, error) {
+ request := &ActionRequest{"type": "power_off"}
+ 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(ctx context.Context, id int) (*Action, *Response, error) {
+ request := &ActionRequest{"type": "power_on"}
+ 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(ctx context.Context, id int) (*Action, *Response, error) {
+ request := &ActionRequest{"type": "power_cycle"}
+ 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(ctx context.Context, id int) (*Action, *Response, error) {
+ request := &ActionRequest{"type": "reboot"}
+ return s.doAction(ctx, id, request)
+}
+
+// Restore an image to a Droplet
+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(ctx, id, request)
+}
+
+// Resize a Droplet
+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(ctx, id, request)
+}
+
+// Rename a Droplet
+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(ctx, id, request)
+}
+
+// Snapshot a Droplet.
+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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+// 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(ctx, id, request)
+}
+
+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")
+ }
+
+ if request == nil {
+ return nil, nil, NewArgError("request", "request can't be nil")
+ }
+
+ path := dropletActionPath(id)
+
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Event, resp, err
+}
+
+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, http.MethodPost, path, request)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Actions, 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")
+ }
+
+ if actionID < 1 {
+ return nil, nil, NewArgError("actionID", "cannot be less than 1")
+ }
+
+ path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID)
+ return s.get(ctx, path)
+}
+
+// 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(ctx, u.Path)
+
+}
+
+func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Event, resp, err
+
+}
+
+func dropletActionPath(dropletID int) string {
+ return fmt.Sprintf("v2/droplets/%d/actions", dropletID)
+}
+
+func dropletActionPathByTag(tag string) string {
+ return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag)
+}
diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go
new file mode 100644
index 000000000..5ec898b2c
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/droplets.go
@@ -0,0 +1,567 @@
+package godo
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const dropletBasePath = "v2/droplets"
+
+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(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
+// DigitalOcean API.
+type DropletsServiceOp struct {
+ client *Client
+}
+
+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"`
+ 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
+type Kernel struct {
+ ID int `json:"id,float64,omitempty"`
+ Name string `json:"name,omitempty"`
+ Version string `json:"version,omitempty"`
+}
+
+// BackupWindow object
+type BackupWindow struct {
+ Start *Timestamp `json:"start,omitempty"`
+ End *Timestamp `json:"end,omitempty"`
+}
+
+// Convert Droplet to a string
+func (d Droplet) String() string {
+ return Stringify(d)
+}
+
+// DropletRoot represents a Droplet root
+type dropletRoot struct {
+ Droplet *Droplet `json:"droplet"`
+ Links *Links `json:"links,omitempty"`
+}
+
+type dropletsRoot struct {
+ Droplets []Droplet `json:"droplets"`
+ Links *Links `json:"links"`
+}
+
+type kernelsRoot struct {
+ Kernels []Kernel `json:"kernels,omitempty"`
+ Links *Links `json:"links"`
+}
+
+type dropletSnapshotsRoot struct {
+ Snapshots []Image `json:"snapshots,omitempty"`
+ Links *Links `json:"links"`
+}
+
+type backupsRoot struct {
+ Backups []Image `json:"backups,omitempty"`
+ Links *Links `json:"links"`
+}
+
+// DropletCreateImage identifies an image for the create request. It prefers slug over ID.
+type DropletCreateImage struct {
+ ID int
+ Slug string
+}
+
+// MarshalJSON returns either the slug or id of the image. It returns the id
+// if the slug is empty.
+func (d DropletCreateImage) MarshalJSON() ([]byte, error) {
+ if d.Slug != "" {
+ return json.Marshal(d.Slug)
+ }
+
+ 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
+ Fingerprint string
+}
+
+// MarshalJSON returns either the fingerprint or id of the ssh key. It returns
+// the id if the fingerprint is empty.
+func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) {
+ if d.Fingerprint != "" {
+ return json.Marshal(d.Fingerprint)
+ }
+
+ return json.Marshal(d.ID)
+}
+
+// DropletCreateRequest represents a request to create a Droplet.
+type DropletCreateRequest struct {
+ Name string `json:"name"`
+ 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"`
+ 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)
+}
+
+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.
+type NetworkV4 struct {
+ IPAddress string `json:"ip_address,omitempty"`
+ Netmask string `json:"netmask,omitempty"`
+ Gateway string `json:"gateway,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+func (n NetworkV4) String() string {
+ return Stringify(n)
+}
+
+// NetworkV6 represents a DigitalOcean IPv6 network.
+type NetworkV6 struct {
+ IPAddress string `json:"ip_address,omitempty"`
+ Netmask int `json:"netmask,omitempty"`
+ Gateway string `json:"gateway,omitempty"`
+ Type string `json:"type,omitempty"`
+}
+
+func (n NetworkV6) String() string {
+ return Stringify(n)
+}
+
+// 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Droplets, resp, err
+}
+
+// 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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Droplet, resp, err
+}
+
+// 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(ctx, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Droplet, resp, err
+}
+
+// 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 := dropletBasePath
+
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletsRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// 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")
+ }
+
+ path := fmt.Sprintf("%s/%d/kernels", dropletBasePath, dropletID)
+ path, err := addOptions(path, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(kernelsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Kernels, resp, err
+}
+
+// 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")
+ }
+
+ path := fmt.Sprintf("%s/%d/actions", dropletBasePath, dropletID)
+ path, err := addOptions(path, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Actions, resp, err
+}
+
+// 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")
+ }
+
+ path := fmt.Sprintf("%s/%d/backups", dropletBasePath, dropletID)
+ path, err := addOptions(path, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(backupsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Backups, resp, err
+}
+
+// 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")
+ }
+
+ path := fmt.Sprintf("%s/%d/snapshots", dropletBasePath, dropletID)
+ path, err := addOptions(path, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletSnapshotsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Snapshots, resp, err
+}
+
+// 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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(dropletsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Droplets, resp, err
+}
+
+func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) {
+ action, _, err := s.client.DropletActions.GetByURI(ctx, uri)
+
+ if err != nil {
+ return "", err
+ }
+
+ return action.Status, nil
+}
diff --git a/vendor/github.com/digitalocean/godo/errors.go b/vendor/github.com/digitalocean/godo/errors.go
new file mode 100644
index 000000000..a65ebd76b
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/errors.go
@@ -0,0 +1,24 @@
+package godo
+
+import "fmt"
+
+// ArgError is an error that represents an error with an input to godo. It
+// identifies the argument and the cause (if possible).
+type ArgError struct {
+ arg string
+ reason string
+}
+
+var _ error = &ArgError{}
+
+// NewArgError creates an InputError.
+func NewArgError(arg, reason string) *ArgError {
+ return &ArgError{
+ arg: arg,
+ reason: reason,
+ }
+}
+
+func (e *ArgError) Error() string {
+ return fmt.Sprintf("%s is invalid because %s", e.arg, e.reason)
+}
diff --git a/vendor/github.com/digitalocean/godo/firewalls.go b/vendor/github.com/digitalocean/godo/firewalls.go
new file mode 100644
index 000000000..afc0af323
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/firewalls.go
@@ -0,0 +1,264 @@
+package godo
+
+import (
+ "net/http"
+ "path"
+ "strconv"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const firewallsBasePath = "/v2/firewalls"
+
+// FirewallsService is an interface for managing Firewalls with the DigitalOcean API.
+// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls
+type FirewallsService interface {
+ Get(context.Context, string) (*Firewall, *Response, error)
+ Create(context.Context, *FirewallRequest) (*Firewall, *Response, error)
+ Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error)
+ Delete(context.Context, string) (*Response, error)
+ List(context.Context, *ListOptions) ([]Firewall, *Response, error)
+ ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error)
+ AddDroplets(context.Context, string, ...int) (*Response, error)
+ RemoveDroplets(context.Context, string, ...int) (*Response, error)
+ AddTags(context.Context, string, ...string) (*Response, error)
+ RemoveTags(context.Context, string, ...string) (*Response, error)
+ AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
+ RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
+}
+
+// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API.
+type FirewallsServiceOp struct {
+ client *Client
+}
+
+// Firewall represents a DigitalOcean Firewall configuration.
+type Firewall struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+ DropletIDs []int `json:"droplet_ids"`
+ Tags []string `json:"tags"`
+ Created string `json:"created_at"`
+ PendingChanges []PendingChange `json:"pending_changes"`
+}
+
+// String creates a human-readable description of a Firewall.
+func (fw Firewall) String() string {
+ return Stringify(fw)
+}
+
+// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
+type FirewallRequest struct {
+ Name string `json:"name"`
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+ DropletIDs []int `json:"droplet_ids"`
+ Tags []string `json:"tags"`
+}
+
+// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall.
+type FirewallRulesRequest struct {
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+}
+
+// InboundRule represents a DigitalOcean Firewall inbound rule.
+type InboundRule struct {
+ Protocol string `json:"protocol,omitempty"`
+ PortRange string `json:"ports,omitempty"`
+ Sources *Sources `json:"sources"`
+}
+
+// OutboundRule represents a DigitalOcean Firewall outbound rule.
+type OutboundRule struct {
+ Protocol string `json:"protocol,omitempty"`
+ PortRange string `json:"ports,omitempty"`
+ Destinations *Destinations `json:"destinations"`
+}
+
+// Sources represents a DigitalOcean Firewall InboundRule sources.
+type Sources struct {
+ Addresses []string `json:"addresses,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ DropletIDs []int `json:"droplet_ids,omitempty"`
+ LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
+}
+
+// PendingChange represents a DigitalOcean Firewall status details.
+type PendingChange struct {
+ DropletID int `json:"droplet_id,omitempty"`
+ Removing bool `json:"removing,omitempty"`
+ Status string `json:"status,omitempty"`
+}
+
+// Destinations represents a DigitalOcean Firewall OutboundRule destinations.
+type Destinations struct {
+ Addresses []string `json:"addresses,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ DropletIDs []int `json:"droplet_ids,omitempty"`
+ LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
+}
+
+var _ FirewallsService = &FirewallsServiceOp{}
+
+// Get an existing Firewall by its identifier.
+func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+
+ req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Create a new Firewall with a given configuration.
+func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) {
+ req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Update an existing Firewall with new configuration.
+func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+
+ req, err := fw.client.NewRequest(ctx, "PUT", path, fr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Delete a Firewall by its identifier.
+func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, nil)
+}
+
+// List Firewalls.
+func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) {
+ path, err := addOptions(firewallsBasePath, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return fw.listHelper(ctx, path)
+}
+
+// ListByDroplet Firewalls.
+func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) {
+ basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls")
+ path, err := addOptions(basePath, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return fw.listHelper(ctx, path)
+}
+
+// AddDroplets to a Firewall.
+func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "droplets")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs})
+}
+
+// RemoveDroplets from a Firewall.
+func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "droplets")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs})
+}
+
+// AddTags to a Firewall.
+func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "tags")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags})
+}
+
+// RemoveTags from a Firewall.
+func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "tags")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags})
+}
+
+// AddRules to a Firewall.
+func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "rules")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, rr)
+}
+
+// RemoveRules from a Firewall.
+func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "rules")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, rr)
+}
+
+type dropletsRequest struct {
+ IDs []int `json:"droplet_ids"`
+}
+
+type tagsRequest struct {
+ Tags []string `json:"tags"`
+}
+
+type firewallRoot struct {
+ Firewall *Firewall `json:"firewall"`
+}
+
+type firewallsRoot struct {
+ Firewalls []Firewall `json:"firewalls"`
+ Links *Links `json:"links"`
+}
+
+func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) {
+ req, err := fw.client.NewRequest(ctx, method, path, v)
+ if err != nil {
+ return nil, err
+ }
+
+ return fw.client.Do(ctx, req, nil)
+}
+
+func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) {
+ req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallsRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Firewalls, resp, err
+}
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 000000000..a9187dca7
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/floating_ips.go
@@ -0,0 +1,136 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(floatingIPsRoot)
+ resp, err := f.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(floatingIPRoot)
+ resp, err := f.client.Do(ctx, 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, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(floatingIPRoot)
+ resp, err := f.client.Do(ctx, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := f.client.Do(ctx, 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 000000000..2fd2393ce
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/floating_ips_actions.go
@@ -0,0 +1,110 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+// 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, http.MethodPost, path, request)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, 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/godo.go b/vendor/github.com/digitalocean/godo/godo.go
new file mode 100644
index 000000000..9dfef2a31
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -0,0 +1,400 @@
+package godo
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "reflect"
+ "strconv"
+ "time"
+
+ "github.com/google/go-querystring/query"
+ headerLink "github.com/tent/http-link-go"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const (
+ libraryVersion = "1.1.0"
+ defaultBaseURL = "https://api.digitalocean.com/"
+ userAgent = "godo/" + libraryVersion
+ mediaType = "application/json"
+
+ headerRateLimit = "RateLimit-Limit"
+ headerRateRemaining = "RateLimit-Remaining"
+ headerRateReset = "RateLimit-Reset"
+)
+
+// Client manages communication with DigitalOcean V2 API.
+type Client struct {
+ // HTTP client used to communicate with the DO API.
+ client *http.Client
+
+ // Base URL for API requests.
+ BaseURL *url.URL
+
+ // User agent for client
+ UserAgent string
+
+ // Rate contains the current rate limit for the client as determined by the most recent
+ // API call.
+ Rate Rate
+
+ // Services used for communicating with the API
+ 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
+ Firewalls FirewallsService
+
+ // Optional function called after every successful request made to the DO APIs
+ onRequestCompleted RequestCompletionCallback
+}
+
+// RequestCompletionCallback defines the type of the request callback function
+type RequestCompletionCallback func(*http.Request, *http.Response)
+
+// ListOptions specifies the optional parameters to various List methods that
+// support pagination.
+type ListOptions struct {
+ // For paginated result sets, page of results to retrieve.
+ Page int `url:"page,omitempty"`
+
+ // For paginated result sets, the number of results to include per page.
+ PerPage int `url:"per_page,omitempty"`
+}
+
+// Response is a DigitalOcean response. This wraps the standard http.Response returned from DigitalOcean.
+type Response struct {
+ *http.Response
+
+ // Links that were returned with the response. These are parsed from
+ // request body and not the header.
+ Links *Links
+
+ // Monitoring URI
+ Monitor string
+
+ Rate
+}
+
+// An ErrorResponse reports the error caused by an API request
+type ErrorResponse struct {
+ // HTTP response that caused this error
+ Response *http.Response
+
+ // Error message
+ Message string `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.
+type Rate struct {
+ // The number of request per hour the client is currently limited to.
+ Limit int `json:"limit"`
+
+ // The number of remaining requests the client can make this hour.
+ Remaining int `json:"remaining"`
+
+ // The time at which the current rate limit will reset.
+ Reset Timestamp `json:"reset"`
+}
+
+func addOptions(s string, opt interface{}) (string, error) {
+ v := reflect.ValueOf(opt)
+
+ if v.Kind() == reflect.Ptr && v.IsNil() {
+ return s, nil
+ }
+
+ origURL, err := url.Parse(s)
+ if err != nil {
+ return s, err
+ }
+
+ origValues := origURL.Query()
+
+ newValues, err := query.Values(opt)
+ if err != nil {
+ return s, err
+ }
+
+ for k, v := range newValues {
+ origValues[k] = v
+ }
+
+ origURL.RawQuery = origValues.Encode()
+ return origURL.String(), nil
+}
+
+// NewClient returns a new DigitalOcean API client.
+func NewClient(httpClient *http.Client) *Client {
+ if httpClient == nil {
+ httpClient = http.DefaultClient
+ }
+
+ baseURL, _ := url.Parse(defaultBaseURL)
+
+ c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
+ c.Account = &AccountServiceOp{client: c}
+ c.Actions = &ActionsServiceOp{client: c}
+ 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}
+ c.Firewalls = &FirewallsServiceOp{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(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+
+ u := c.BaseURL.ResolveReference(rel)
+
+ buf := new(bytes.Buffer)
+ if body != nil {
+ err = json.NewEncoder(buf).Encode(body)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ req, err := http.NewRequest(method, u.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Content-Type", mediaType)
+ req.Header.Add("Accept", mediaType)
+ req.Header.Add("User-Agent", c.UserAgent)
+ return req, nil
+}
+
+// OnRequestCompleted sets the DO API request completion callback
+func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
+ c.onRequestCompleted = rc
+}
+
+// newResponse creates a new Response for the provided http.Response
+func newResponse(r *http.Response) *Response {
+ response := Response{Response: r}
+ response.populateRate()
+
+ return &response
+}
+
+func (r *Response) links() (map[string]headerLink.Link, error) {
+ if linkText, ok := r.Response.Header["Link"]; ok {
+ links, err := headerLink.Parse(linkText[0])
+
+ if err != nil {
+ return nil, err
+ }
+
+ linkMap := map[string]headerLink.Link{}
+ for _, link := range links {
+ linkMap[link.Rel] = link
+ }
+
+ return linkMap, nil
+ }
+
+ return map[string]headerLink.Link{}, nil
+}
+
+// populateRate parses the rate related headers and populates the response Rate.
+func (r *Response) populateRate() {
+ if limit := r.Header.Get(headerRateLimit); limit != "" {
+ r.Rate.Limit, _ = strconv.Atoi(limit)
+ }
+ if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
+ r.Rate.Remaining, _ = strconv.Atoi(remaining)
+ }
+ if reset := r.Header.Get(headerRateReset); reset != "" {
+ if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
+ r.Rate.Reset = Timestamp{time.Unix(v, 0)}
+ }
+ }
+}
+
+// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
+// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
+// the raw response will be written to v, without attempting to decode it.
+func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
+ resp, err := context.DoRequestWithClient(ctx, c.client, req)
+ if err != nil {
+ return nil, err
+ }
+ if c.onRequestCompleted != nil {
+ c.onRequestCompleted(req, resp)
+ }
+
+ defer func() {
+ if rerr := resp.Body.Close(); err == nil {
+ err = rerr
+ }
+ }()
+
+ response := newResponse(resp)
+ c.Rate = response.Rate
+
+ err = CheckResponse(resp)
+ if err != nil {
+ return response, err
+ }
+
+ if v != nil {
+ if w, ok := v.(io.Writer); ok {
+ _, err = io.Copy(w, resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(v)
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ 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)
+}
+
+// CheckResponse checks the API response for errors, and returns them if present. A response is considered an
+// error if it has a status code outside the 200 range. API error responses are expected to have either no response
+// body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored.
+func CheckResponse(r *http.Response) error {
+ if c := r.StatusCode; c >= 200 && c <= 299 {
+ return nil
+ }
+
+ errorResponse := &ErrorResponse{Response: r}
+ data, err := ioutil.ReadAll(r.Body)
+ if err == nil && len(data) > 0 {
+ err := json.Unmarshal(data, errorResponse)
+ if err != nil {
+ return err
+ }
+ }
+
+ return errorResponse
+}
+
+func (r Rate) String() string {
+ return Stringify(r)
+}
+
+// String is a helper routine that allocates a new string value
+// to store v and returns a pointer to it.
+func String(v string) *string {
+ p := new(string)
+ *p = v
+ return p
+}
+
+// Int is a helper routine that allocates a new int32 value
+// to store v and returns a pointer to it, but unlike Int32
+// its argument value is an int.
+func Int(v int) *int {
+ p := new(int)
+ *p = v
+ return p
+}
+
+// Bool is a helper routine that allocates a new bool value
+// to store v and returns a pointer to it.
+func Bool(v bool) *bool {
+ p := new(bool)
+ *p = v
+ return p
+}
+
+// StreamToString converts a reader to a string
+func StreamToString(stream io.Reader) string {
+ buf := new(bytes.Buffer)
+ _, _ = buf.ReadFrom(stream)
+ return buf.String()
+}
diff --git a/vendor/github.com/digitalocean/godo/image_actions.go b/vendor/github.com/digitalocean/godo/image_actions.go
new file mode 100644
index 000000000..1e9fa3122
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/image_actions.go
@@ -0,0 +1,103 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+// 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(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
+// DigitalOcean API.
+type ImageActionsServiceOp struct {
+ client *Client
+}
+
+var _ ImageActionsService = &ImageActionsServiceOp{}
+
+// Transfer an image
+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")
+ }
+
+ if transferRequest == nil {
+ return nil, nil, NewArgError("transferRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("v2/images/%d/actions", imageID)
+
+ req, err := i.client.NewRequest(ctx, http.MethodPost, path, transferRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := i.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, 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, http.MethodPost, path, convertRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := i.client.Do(ctx, 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(ctx context.Context, imageID, actionID int) (*Action, *Response, error) {
+ if imageID < 1 {
+ return nil, nil, NewArgError("imageID", "cannot be less than 1")
+ }
+
+ if actionID < 1 {
+ return nil, nil, NewArgError("actionID", "cannot be less than 1")
+ }
+
+ path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
+
+ req, err := i.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := i.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Event, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/images.go b/vendor/github.com/digitalocean/godo/images.go
new file mode 100644
index 000000000..af989f41d
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/images.go
@@ -0,0 +1,199 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const imageBasePath = "v2/images"
+
+// ImagesService is an interface for interfacing with the images
+// endpoints of the DigitalOcean API
+// See: https://developers.digitalocean.com/documentation/v2#images
+type ImagesService interface {
+ 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
+// DigitalOcean API.
+type ImagesServiceOp struct {
+ client *Client
+}
+
+var _ ImagesService = &ImagesServiceOp{}
+
+// Image represents a DigitalOcean Image
+type Image struct {
+ ID int `json:"id,float64,omitempty"`
+ Name string `json:"name,omitempty"`
+ Type string `json:"type,omitempty"`
+ Distribution string `json:"distribution,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Public bool `json:"public,omitempty"`
+ Regions []string `json:"regions,omitempty"`
+ MinDiskSize int `json:"min_disk_size,omitempty"`
+ Created string `json:"created_at,omitempty"`
+}
+
+// ImageUpdateRequest represents a request to update an image.
+type ImageUpdateRequest struct {
+ Name string `json:"name"`
+}
+
+type imageRoot struct {
+ Image *Image
+}
+
+type imagesRoot struct {
+ Images []Image
+ Links *Links `json:"links"`
+}
+
+type listImageOptions struct {
+ Private bool `url:"private,omitempty"`
+ Type string `url:"type,omitempty"`
+}
+
+func (i Image) String() string {
+ return Stringify(i)
+}
+
+// List lists all the images available.
+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(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
+ listOpt := listImageOptions{Type: "distribution"}
+ return s.list(ctx, opt, &listOpt)
+}
+
+// ListApplication lists all the application images.
+func (s *ImagesServiceOp) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
+ listOpt := listImageOptions{Type: "application"}
+ return s.list(ctx, opt, &listOpt)
+}
+
+// ListUser lists all the user images.
+func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
+ listOpt := listImageOptions{Private: true}
+ return s.list(ctx, opt, &listOpt)
+}
+
+// GetByID retrieves an image by id.
+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(ctx, interface{}(imageID))
+}
+
+// GetBySlug retrieves an image by slug.
+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(ctx, interface{}(slug))
+}
+
+// Update an image name.
+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")
+ }
+
+ if updateRequest == nil {
+ return nil, nil, NewArgError("updateRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
+ req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(imageRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Image, resp, err
+}
+
+// Delete an image.
+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(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// Helper method for getting an individual image
+func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
+ path := fmt.Sprintf("%s/%v", imageBasePath, ID)
+
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(imageRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Image, resp, err
+}
+
+// Helper method for listing images
+func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) {
+ path := imageBasePath
+ 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(imagesRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Images, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/keys.go b/vendor/github.com/digitalocean/godo/keys.go
new file mode 100644
index 000000000..2ffdd6ccb
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/keys.go
@@ -0,0 +1,227 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+const keysBasePath = "v2/account/keys"
+
+// KeysService is an interface for interfacing with the keys
+// endpoints of the DigitalOcean API
+// See: https://developers.digitalocean.com/documentation/v2#keys
+type KeysService interface {
+ 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
+// DigitalOcean API.
+type KeysServiceOp struct {
+ client *Client
+}
+
+var _ KeysService = &KeysServiceOp{}
+
+// Key represents a DigitalOcean Key.
+type Key struct {
+ ID int `json:"id,float64,omitempty"`
+ Name string `json:"name,omitempty"`
+ Fingerprint string `json:"fingerprint,omitempty"`
+ PublicKey string `json:"public_key,omitempty"`
+}
+
+// KeyUpdateRequest represents a request to update a DigitalOcean key.
+type KeyUpdateRequest struct {
+ Name string `json:"name"`
+}
+
+type keysRoot struct {
+ SSHKeys []Key `json:"ssh_keys"`
+ Links *Links `json:"links"`
+}
+
+type keyRoot struct {
+ SSHKey *Key `json:"ssh_key"`
+}
+
+func (s Key) String() string {
+ return Stringify(s)
+}
+
+// KeyCreateRequest represents a request to create a new key.
+type KeyCreateRequest struct {
+ Name string `json:"name"`
+ PublicKey string `json:"public_key"`
+}
+
+// List all keys
+func (s *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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(keysRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.SSHKeys, resp, err
+}
+
+// Performs a get given a path
+func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(keyRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.SSHKey, resp, err
+}
+
+// GetByID gets a Key by id
+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(ctx, path)
+}
+
+// GetByFingerprint gets a Key by by fingerprint
+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(ctx, path)
+}
+
+// Create a key using a KeyCreateRequest
+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(ctx, http.MethodPost, keysBasePath, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(keyRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.SSHKey, resp, err
+}
+
+// UpdateByID updates a key name by ID.
+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")
+ }
+
+ if updateRequest == nil {
+ return nil, nil, NewArgError("updateRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
+ req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(keyRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.SSHKey, resp, err
+}
+
+// UpdateByFingerprint updates a key name by fingerprint.
+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")
+ }
+
+ if updateRequest == nil {
+ return nil, nil, NewArgError("updateRequest", "cannot be nil")
+ }
+
+ path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
+ req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(keyRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.SSHKey, resp, err
+}
+
+// Delete key using a path
+func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// DeleteByID deletes a key by its id
+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(ctx, path)
+}
+
+// DeleteByFingerprint deletes a key by its fingerprint
+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(ctx, path)
+}
diff --git a/vendor/github.com/digitalocean/godo/links.go b/vendor/github.com/digitalocean/godo/links.go
new file mode 100644
index 000000000..0c6110210
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/links.go
@@ -0,0 +1,84 @@
+package godo
+
+import (
+ "net/url"
+ "strconv"
+
+ "github.com/digitalocean/godo/context"
+)
+
+// Links manages links that are returned along with a List
+type Links struct {
+ Pages *Pages `json:"pages,omitempty"`
+ Actions []LinkAction `json:"actions,omitempty"`
+}
+
+// Pages are pages specified in Links
+type Pages struct {
+ First string `json:"first,omitempty"`
+ Prev string `json:"prev,omitempty"`
+ Last string `json:"last,omitempty"`
+ Next string `json:"next,omitempty"`
+}
+
+// LinkAction is a pointer to an action
+type LinkAction struct {
+ ID int `json:"id,omitempty"`
+ Rel string `json:"rel,omitempty"`
+ HREF string `json:"href,omitempty"`
+}
+
+// CurrentPage is current page of the list
+func (l *Links) CurrentPage() (int, error) {
+ return l.Pages.current()
+}
+
+func (p *Pages) current() (int, error) {
+ switch {
+ case p == nil:
+ return 1, nil
+ case p.Prev == "" && p.Next != "":
+ return 1, nil
+ case p.Prev != "":
+ prevPage, err := pageForURL(p.Prev)
+ if err != nil {
+ return 0, err
+ }
+
+ return prevPage + 1, nil
+ }
+
+ return 0, nil
+}
+
+// IsLastPage returns true if the current page is the last
+func (l *Links) IsLastPage() bool {
+ if l.Pages == nil {
+ return true
+ }
+ return l.Pages.isLast()
+}
+
+func (p *Pages) isLast() bool {
+ return p.Last == ""
+}
+
+func pageForURL(urlText string) (int, error) {
+ u, err := url.ParseRequestURI(urlText)
+ if err != nil {
+ return 0, err
+ }
+
+ pageStr := u.Query().Get("page")
+ page, err := strconv.Atoi(pageStr)
+ if err != nil {
+ return 0, err
+ }
+
+ return page, nil
+}
+
+// Get a link action by 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 000000000..b4b5300b3
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/load_balancers.go
@@ -0,0 +1,278 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(loadBalancerRoot)
+ resp, err := l.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(loadBalancersRoot)
+ resp, err := l.client.Do(ctx, 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, http.MethodPost, loadBalancersBasePath, lbr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(loadBalancerRoot)
+ resp, err := l.client.Do(ctx, 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(ctx, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return l.client.Do(ctx, 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, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
+ if err != nil {
+ return nil, err
+ }
+
+ return l.client.Do(ctx, 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, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
+ if err != nil {
+ return nil, err
+ }
+
+ return l.client.Do(ctx, 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, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
+ if err != nil {
+ return nil, err
+ }
+
+ return l.client.Do(ctx, 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, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
+ if err != nil {
+ return nil, err
+ }
+
+ return l.client.Do(ctx, req, nil)
+}
diff --git a/vendor/github.com/digitalocean/godo/regions.go b/vendor/github.com/digitalocean/godo/regions.go
new file mode 100644
index 000000000..de7603fd5
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/regions.go
@@ -0,0 +1,65 @@
+package godo
+
+import (
+ "net/http"
+
+ "github.com/digitalocean/godo/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(context.Context, *ListOptions) ([]Region, *Response, error)
+}
+
+// RegionsServiceOp handles communication with the region related methods of the
+// DigitalOcean API.
+type RegionsServiceOp struct {
+ client *Client
+}
+
+var _ RegionsService = &RegionsServiceOp{}
+
+// Region represents a DigitalOcean Region
+type Region struct {
+ Slug string `json:"slug,omitempty"`
+ Name string `json:"name,omitempty"`
+ Sizes []string `json:"sizes,omitempty"`
+ Available bool `json:"available,omitempty"`
+ Features []string `json:"features,omitempty"`
+}
+
+type regionsRoot struct {
+ Regions []Region
+ Links *Links `json:"links"`
+}
+
+func (r Region) String() string {
+ return Stringify(r)
+}
+
+// List all regions
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(regionsRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Regions, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/sizes.go b/vendor/github.com/digitalocean/godo/sizes.go
new file mode 100644
index 000000000..f0c5a051f
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/sizes.go
@@ -0,0 +1,69 @@
+package godo
+
+import (
+ "net/http"
+
+ "github.com/digitalocean/godo/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(context.Context, *ListOptions) ([]Size, *Response, error)
+}
+
+// SizesServiceOp handles communication with the size related methods of the
+// DigitalOcean API.
+type SizesServiceOp struct {
+ client *Client
+}
+
+var _ SizesService = &SizesServiceOp{}
+
+// Size represents a DigitalOcean Size
+type Size struct {
+ Slug string `json:"slug,omitempty"`
+ Memory int `json:"memory,omitempty"`
+ Vcpus int `json:"vcpus,omitempty"`
+ Disk int `json:"disk,omitempty"`
+ PriceMonthly float64 `json:"price_monthly,omitempty"`
+ PriceHourly float64 `json:"price_hourly,omitempty"`
+ Regions []string `json:"regions,omitempty"`
+ Available bool `json:"available,omitempty"`
+ Transfer float64 `json:"transfer,omitempty"`
+}
+
+func (s Size) String() string {
+ return Stringify(s)
+}
+
+type sizesRoot struct {
+ Sizes []Size
+ Links *Links `json:"links"`
+}
+
+// List all images
+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(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(sizesRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Sizes, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/snapshots.go b/vendor/github.com/digitalocean/godo/snapshots.go
new file mode 100644
index 000000000..8a42aed38
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/snapshots.go
@@ -0,0 +1,141 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(snapshotRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(snapshotsRoot)
+ resp, err := s.client.Do(ctx, 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/storage.go b/vendor/github.com/digitalocean/godo/storage.go
new file mode 100644
index 000000000..a888601c4
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/storage.go
@@ -0,0 +1,241 @@
+package godo
+
+import (
+ "fmt"
+ "time"
+
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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"`
+ SnapshotID string `json:"snapshot_id"`
+}
+
+// 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(storageVolumesRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(storageVolumeRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(storageVolumeRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+ return svc.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(snapshotsRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodPost, path, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(snapshotRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(snapshotRoot)
+ resp, err := svc.client.Do(ctx, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+ return svc.client.Do(ctx, 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 000000000..fe916ac85
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/storage_actions.go
@@ -0,0 +1,130 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+// 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)
+ 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)
+}
+
+// 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, http.MethodPost, path, request)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, 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/strings.go b/vendor/github.com/digitalocean/godo/strings.go
new file mode 100644
index 000000000..4a8bfb636
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/strings.go
@@ -0,0 +1,92 @@
+package godo
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "reflect"
+)
+
+var timestampType = reflect.TypeOf(Timestamp{})
+
+// Stringify attempts to create a string representation of DigitalOcean types
+func Stringify(message interface{}) string {
+ var buf bytes.Buffer
+ v := reflect.ValueOf(message)
+ stringifyValue(&buf, v)
+ return buf.String()
+}
+
+// stringifyValue was graciously cargoculted from the goprotubuf library
+func stringifyValue(w io.Writer, val reflect.Value) {
+ if val.Kind() == reflect.Ptr && val.IsNil() {
+ _, _ = w.Write([]byte(""))
+ return
+ }
+
+ v := reflect.Indirect(val)
+
+ switch v.Kind() {
+ case reflect.String:
+ fmt.Fprintf(w, `"%s"`, v)
+ case reflect.Slice:
+ stringifySlice(w, v)
+ return
+ case reflect.Struct:
+ stringifyStruct(w, v)
+ default:
+ if v.CanInterface() {
+ fmt.Fprint(w, v.Interface())
+ }
+ }
+}
+
+func stringifySlice(w io.Writer, v reflect.Value) {
+ _, _ = w.Write([]byte{'['})
+ 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 000000000..6427488db
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/tags.go
@@ -0,0 +1,209 @@
+package godo
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/digitalocean/godo/context"
+)
+
+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)
+ 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"`
+}
+
+// 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(tagsRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(tagRoot)
+ resp, err := s.client.Do(ctx, 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, http.MethodPost, tagsBasePath, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(tagRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Tag, 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, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, 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, http.MethodPost, path, tagRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, 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, http.MethodDelete, path, untagRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := s.client.Do(ctx, req, nil)
+
+ return resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/timestamp.go b/vendor/github.com/digitalocean/godo/timestamp.go
new file mode 100644
index 000000000..37a28e5f2
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/timestamp.go
@@ -0,0 +1,35 @@
+package godo
+
+import (
+ "strconv"
+ "time"
+)
+
+// Timestamp represents a time that can be unmarshalled from a JSON string
+// formatted as either an RFC3339 or Unix timestamp. All
+// exported methods of time.Time can be called on Timestamp.
+type Timestamp struct {
+ time.Time
+}
+
+func (t Timestamp) String() string {
+ return t.Time.String()
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+// Time is expected in RFC3339 or Unix format.
+func (t *Timestamp) UnmarshalJSON(data []byte) error {
+ str := string(data)
+ i, err := strconv.ParseInt(str, 10, 64)
+ if err == nil {
+ t.Time = time.Unix(i, 0)
+ } else {
+ t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
+ }
+ return err
+}
+
+// Equal reports whether t and u are equal based on time.Equal
+func (t Timestamp) Equal(u Timestamp) bool {
+ return t.Time.Equal(u.Time)
+}
diff --git a/vendor/github.com/tent/http-link-go/LICENSE b/vendor/github.com/tent/http-link-go/LICENSE
new file mode 100644
index 000000000..88dcd4afd
--- /dev/null
+++ b/vendor/github.com/tent/http-link-go/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Tent.is, LLC. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Tent.is, LLC nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/tent/http-link-go/README.md b/vendor/github.com/tent/http-link-go/README.md
new file mode 100644
index 000000000..07d470e4d
--- /dev/null
+++ b/vendor/github.com/tent/http-link-go/README.md
@@ -0,0 +1,12 @@
+# http-link-go [](https://travis-ci.org/tent/http-link-go)
+
+http-link-go implements parsing and serialization of Link header values as
+defined in [RFC 5988](https://tools.ietf.org/html/rfc5988).
+
+[**Documentation**](http://godoc.org/github.com/tent/http-link-go)
+
+## Installation
+
+```text
+go get github.com/tent/http-link-go
+```
diff --git a/vendor/github.com/tent/http-link-go/link.go b/vendor/github.com/tent/http-link-go/link.go
new file mode 100644
index 000000000..584dfd051
--- /dev/null
+++ b/vendor/github.com/tent/http-link-go/link.go
@@ -0,0 +1,185 @@
+// Package link implements parsing and serialization of Link header values as
+// defined in RFC 5988.
+package link
+
+import (
+ "bytes"
+ "errors"
+ "sort"
+ "unicode"
+)
+
+type Link struct {
+ URI string
+ Rel string
+ Params map[string]string
+}
+
+// Format serializes a slice of Links into a header value. It does not currently
+// implement RFC 2231 handling of non-ASCII character encoding and language
+// information.
+func Format(links []Link) string {
+ buf := &bytes.Buffer{}
+ for i, link := range links {
+ if i > 0 {
+ buf.Write([]byte(", "))
+ }
+ buf.WriteByte('<')
+ buf.WriteString(link.URI)
+ buf.WriteByte('>')
+
+ writeParam(buf, "rel", link.Rel)
+
+ keys := make([]string, 0, len(link.Params))
+ for k := range link.Params {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ for _, k := range keys {
+ writeParam(buf, k, link.Params[k])
+ }
+ }
+
+ return buf.String()
+}
+
+func writeParam(buf *bytes.Buffer, key, value string) {
+ buf.Write([]byte("; "))
+ buf.WriteString(key)
+ buf.Write([]byte(`="`))
+ buf.WriteString(value)
+ buf.WriteByte('"')
+}
+
+// Parse parses a Link header value into a slice of Links. It does not currently
+// implement RFC 2231 handling of non-ASCII character encoding and language
+// information.
+func Parse(l string) ([]Link, error) {
+ v := []byte(l)
+ v = bytes.TrimSpace(v)
+ if len(v) == 0 {
+ return nil, nil
+ }
+
+ links := make([]Link, 0, 1)
+ for len(v) > 0 {
+ if v[0] != '<' {
+ return nil, errors.New("link: does not start with <")
+ }
+ lend := bytes.IndexByte(v, '>')
+ if lend == -1 {
+ return nil, errors.New("link: does not contain ending >")
+ }
+
+ params := make(map[string]string)
+ link := Link{URI: string(v[1:lend]), Params: params}
+ links = append(links, link)
+
+ // trim off parsed url
+ v = v[lend+1:]
+ if len(v) == 0 {
+ break
+ }
+ v = bytes.TrimLeftFunc(v, unicode.IsSpace)
+
+ for len(v) > 0 {
+ if v[0] != ';' && v[0] != ',' {
+ return nil, errors.New(`link: expected ";" or "'", got "` + string(v[0:1]) + `"`)
+ }
+ var next bool
+ if v[0] == ',' {
+ next = true
+ }
+ v = bytes.TrimLeftFunc(v[1:], unicode.IsSpace)
+ if next || len(v) == 0 {
+ break
+ }
+ var key, value []byte
+ key, value, v = consumeParam(v)
+ if key == nil || value == nil {
+ return nil, errors.New("link: malformed param")
+ }
+ if k := string(key); k == "rel" {
+ if links[len(links)-1].Rel == "" {
+ links[len(links)-1].Rel = string(value)
+ }
+ } else {
+ params[k] = string(value)
+ }
+ v = bytes.TrimLeftFunc(v, unicode.IsSpace)
+ }
+ }
+
+ return links, nil
+}
+
+func isTokenChar(r rune) bool {
+ return r > 0x20 && r < 0x7f && r != '"' && r != ',' && r != '=' && r != ';'
+}
+
+func isNotTokenChar(r rune) bool { return !isTokenChar(r) }
+
+func consumeToken(v []byte) (token, rest []byte) {
+ notPos := bytes.IndexFunc(v, isNotTokenChar)
+ if notPos == -1 {
+ return v, nil
+ }
+ if notPos == 0 {
+ return nil, v
+ }
+ return v[0:notPos], v[notPos:]
+}
+
+func consumeValue(v []byte) (value, rest []byte) {
+ if v[0] != '"' {
+ return nil, v
+ }
+
+ rest = v[1:]
+ buffer := &bytes.Buffer{}
+ var nextIsLiteral bool
+ for idx, r := range string(rest) {
+ switch {
+ case nextIsLiteral:
+ buffer.WriteRune(r)
+ nextIsLiteral = false
+ case r == '"':
+ return buffer.Bytes(), rest[idx+1:]
+ case r == '\\':
+ nextIsLiteral = true
+ case r != '\r' && r != '\n':
+ buffer.WriteRune(r)
+ default:
+ return nil, v
+ }
+ }
+ return nil, v
+}
+
+func consumeParam(v []byte) (param, value, rest []byte) {
+ param, rest = consumeToken(v)
+ param = bytes.ToLower(param)
+ if param == nil {
+ return nil, nil, v
+ }
+
+ rest = bytes.TrimLeftFunc(rest, unicode.IsSpace)
+ if len(rest) == 0 || rest[0] != '=' {
+ return nil, nil, v
+ }
+ rest = rest[1:] // consume equals sign
+ rest = bytes.TrimLeftFunc(rest, unicode.IsSpace)
+ if len(rest) == 0 {
+ return nil, nil, v
+ }
+ if rest[0] != '"' {
+ value, rest = consumeToken(rest)
+ } else {
+ value, rest = consumeValue(rest)
+ }
+ if value == nil {
+ return nil, nil, v
+ }
+ return param, value, rest
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 671c3be6b..bd5e6bf77 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -211,6 +211,18 @@
"revision": "7e62cd9a88279c621b6daf38a9ef0dd11c4d47e3",
"revisionTime": "2017-04-24T18:15:40Z"
},
+ {
+ "checksumSHA1": "cGY7ZInpebRCbyfMs9IZbUBHcgE=",
+ "path": "github.com/digitalocean/godo",
+ "revision": "34840385860db94c88d044571153b6a200ca40b2",
+ "revisionTime": "2017-07-06T20:03:01Z"
+ },
+ {
+ "checksumSHA1": "YpWoCsk+u9H5ctWNKKSVPf4b2as=",
+ "path": "github.com/digitalocean/godo/context",
+ "revision": "34840385860db94c88d044571153b6a200ca40b2",
+ "revisionTime": "2017-07-06T20:03:01Z"
+ },
{
"checksumSHA1": "yP+hlSaIhYwxErc/s286p9M+LIs=",
"path": "github.com/dnsimple/dnsimple-go/dnsimple",
@@ -410,6 +422,12 @@
"revision": "3e8091f4417ebaaa3910da63a45ea394ebbfb0e3",
"revisionTime": "2016-04-27T18:05:39Z"
},
+ {
+ "checksumSHA1": "GQ9bu6PuydK3Yor1JgtVKUfEJm8=",
+ "path": "github.com/tent/http-link-go",
+ "revision": "ac974c61c2f990f4115b119354b5e0b47550e888",
+ "revisionTime": "2013-07-02T22:55:49Z"
+ },
{
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
"path": "golang.org/x/net/context",