pax_global_header00006660000000000000000000000064136652700000014511gustar00rootroot0000000000000052 comment=6c3f52d4d7f15e04456df3dad502b3e93af46103 godo-1.37.0/000077500000000000000000000000001366527000000125315ustar00rootroot00000000000000godo-1.37.0/.github/000077500000000000000000000000001366527000000140715ustar00rootroot00000000000000godo-1.37.0/.github/workflows/000077500000000000000000000000001366527000000161265ustar00rootroot00000000000000godo-1.37.0/.github/workflows/ci.yml000066400000000000000000000010551366527000000172450ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: go-pipeline: name: test runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v2 - name: go1.11 test uses: digitalocean/golang-pipeline/go1.11/test@master - name: go1.12 test uses: digitalocean/golang-pipeline/go1.12/test@master - name: go1.13 test uses: digitalocean/golang-pipeline/go1.13/test@master - name: go1.14 test uses: digitalocean/golang-pipeline/go1.14/test@master godo-1.37.0/.gitignore000077500000000000000000000000101366527000000145130ustar00rootroot00000000000000vendor/ godo-1.37.0/.whitesource000066400000000000000000000002121366527000000150660ustar00rootroot00000000000000{ "checkRunSettings": { "vulnerableCheckRunConclusionLevel": "failure" }, "issueSettings": { "minSeverityLevel": "LOW" } }godo-1.37.0/1-click.go000066400000000000000000000024511366527000000143050ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) const oneClickBasePath = "v2/1-clicks" // OneClickService is an interface for interacting with 1-clicks with the // DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2/#1-click-applications type OneClickService interface { List(context.Context, string) ([]*OneClick, *Response, error) } var _ OneClickService = &OneClickServiceOp{} // OneClickServiceOp interfaces with 1-click endpoints in the DigitalOcean API. type OneClickServiceOp struct { client *Client } // OneClick is the structure of a 1-click type OneClick struct { Slug string `json:"slug"` Type string `json:"type"` } // OneClicksRoot is the root of the json payload that contains a list of 1-clicks type OneClicksRoot struct { List []*OneClick `json:"1_clicks"` } // List returns a list of the available 1-click applications. func (ocs *OneClickServiceOp) List(ctx context.Context, oneClickType string) ([]*OneClick, *Response, error) { path := fmt.Sprintf(`%s?type=%s`, oneClickBasePath, oneClickType) req, err := ocs.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(OneClicksRoot) resp, err := ocs.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.List, resp, nil } godo-1.37.0/1-click_test.go000066400000000000000000000013451366527000000153450ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var testOneClick = &OneClick{ Slug: "test-slug", Type: "droplet", } var testOneClickJSON = ` { "slug":"test-slug", "type":"droplet" } ` func TestOneClick_List(t *testing.T) { setup() defer teardown() svc := client.OneClick path := "/v2/1-clicks" want := []*OneClick{ testOneClick, } jsonBlob := ` { "1_clicks": [ ` + testOneClickJSON + ` ] } ` mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jsonBlob) }) got, _, err := svc.List(ctx, "") require.NoError(t, err) assert.Equal(t, want, got) } godo-1.37.0/CHANGELOG.md000066400000000000000000000152521366527000000143470ustar00rootroot00000000000000# Change Log ## [v1.37.0] - 2020-06-01 - #336 registry: URL encode repository names when building URLs. @adamwg - #335 Add 1-click service and request. @scottcrawford03 ## [v1.36.0] - 2020-05-12 - #331 Expose expiry_seconds for Registry.DockerCredentials. @andrewsomething ## [v1.35.1] - 2020-04-21 - #328 Update vulnerable x/crypto dependency - @bentranter ## [v1.35.0] - 2020-04-20 - #326 Add TagCount field to registry/Repository - @nicktate - #325 Add DOCR EA routes - @nicktate - #324 Upgrade godo to Go 1.14 - @bentranter ## [v1.34.0] - 2020-03-30 - #320 Add VPC v3 attributes - @viola ## [v1.33.1] - 2020-03-23 - #318 upgrade github.com/stretchr/objx past 0.1.1 - @hilary ## [v1.33.0] - 2020-03-20 - #310 Add BillingHistory service and List endpoint - @rbutler - #316 load balancers: add new enable_backend_keepalive field - @anitgandhi ## [v1.32.0] - 2020-03-04 - #311 Add reset database user auth method - @zbarahal-do ## [v1.31.0] - 2020-02-28 - #305 invoices: GetPDF and GetCSV methods - @rbutler - #304 Add NewFromToken convenience method to init client - @bentranter - #301 invoices: Get, Summary, and List methods - @rbutler - #299 Fix param expiry_seconds for kubernetes.GetCredentials request - @velp ## [v1.30.0] - 2020-02-03 - #295 registry: support the created_at field - @adamwg - #293 doks: node pool labels - @snormore ## [v1.29.0] - 2019-12-13 - #288 Add Balance Get method - @rbutler - #286,#289 Deserialize meta field - @timoreimann ## [v1.28.0] - 2019-12-04 - #282 Add valid Redis eviction policy constants - @bentranter - #281 Remove databases info from top-level godoc string - @bentranter - #280 Fix VolumeSnapshotResourceType value volumesnapshot -> volume_snapshot - @aqche ## [v1.27.0] - 2019-11-18 - #278 add mysql user auth settings for database users - @gregmankes ## [v1.26.0] - 2019-11-13 - #272 dbaas: get and set mysql sql mode - @mikejholly ## [v1.25.0] - 2019-11-13 - #275 registry/docker-credentials: add support for the read/write parameter - @kamaln7 - #273 implement the registry/docker-credentials endpoint - @kamaln7 - #271 Add registry resource - @snormore ## [v1.24.1] - 2019-11-04 - #264 Update isLast to check p.Next - @aqche ## [v1.24.0] - 2019-10-30 - #267 Return []DatabaseFirewallRule in addition to raw response. - @andrewsomething ## [v1.23.1] - 2019-10-30 - #265 add support for getting/setting firewall rules - @gregmankes - #262 remove ResolveReference call - @mdanzinger - #261 Update CONTRIBUTING.md - @mdanzinger ## [v1.22.0] - 2019-09-24 - #259 Add Kubernetes GetCredentials method - @snormore ## [v1.21.1] - 2019-09-19 - #257 Upgrade to Go 1.13 - @bentranter ## [v1.21.0] - 2019-09-16 - #255 Add DropletID to Kubernetes Node instance - @snormore - #254 Add tags to Database, DatabaseReplica - @Zyqsempai ## [v1.20.0] - 2019-09-06 - #252 Add Kubernetes autoscale config fields - @snormore - #251 Support unset fields on Kubernetes cluster and node pool updates - @snormore - #250 Add Kubernetes GetUser method - @snormore ## [v1.19.0] - 2019-07-19 - #244 dbaas: add private-network-uuid field to create request ## [v1.18.0] - 2019-07-17 - #241 Databases: support for custom VPC UUID on migrate @mikejholly - #240 Add the ability to get URN for a Database @stack72 - #236 Fix omitempty typos in JSON struct tags @amccarthy1 ## [v1.17.0] - 2019-06-21 - #238 Add support for Redis eviction policy in Databases @mikejholly ## [v1.16.0] - 2019-06-04 - #233 Add Kubernetes DeleteNode method, deprecate RecycleNodePoolNodes @bouk ## [v1.15.0] - 2019-05-13 - #231 Add private connection fields to Databases - @mikejholly - #223 Introduce Go modules - @andreiavrammsd ## [v1.14.0] - 2019-05-13 - #229 Add support for upgrading Kubernetes clusters - @adamwg ## [v1.13.0] - 2019-04-19 - #213 Add tagging support for volume snapshots - @jcodybaker ## [v1.12.0] - 2019-04-18 - #224 Add maintenance window support for Kubernetes- @fatih ## [v1.11.1] - 2019-04-04 - #222 Fix Create Database Pools json fields - @sunny-b ## [v1.11.0] - 2019-04-03 - #220 roll out vpc functionality - @jheimann ## [v1.10.1] - 2019-03-27 - #219 Fix Database Pools json field - @sunny-b ## [v1.10.0] - 2019-03-20 - #215 Add support for Databases - @mikejholly ## [v1.9.0] - 2019-03-18 - #214 add support for enable_proxy_protocol. - @mregmi ## [v1.8.0] - 2019-03-13 - #210 Expose tags on storage volume create/list/get. - @jcodybaker ## [v1.7.5] - 2019-03-04 - #207 Add support for custom subdomains for Spaces CDN [beta] - @xornivore ## [v1.7.4] - 2019-02-08 - #202 Allow tagging volumes - @mchitten ## [v1.7.3] - 2018-12-18 - #196 Expose tag support for creating Load Balancers. ## [v1.7.2] - 2018-12-04 - #192 Exposes more options for Kubernetes clusters. ## [v1.7.1] - 2018-11-27 - #190 Expose constants for the state of Kubernetes clusters. ## [v1.7.0] - 2018-11-13 - #188 Kubernetes support [beta] - @aybabtme ## [v1.6.0] - 2018-10-16 - #185 Projects support [beta] - @mchitten ## [v1.5.0] - 2018-10-01 - #181 Adding tagging images support - @hugocorbucci ## [v1.4.2] - 2018-08-30 - #178 Allowing creating domain records with weight of 0 - @TFaga - #177 Adding `VolumeLimit` to account - @lxfontes ## [v1.4.1] - 2018-08-23 - #176 Fix cdn flush cache API endpoint - @sunny-b ## [v1.4.0] - 2018-08-22 - #175 Add support for Spaces CDN - @sunny-b ## [v1.3.0] - 2018-05-24 - #170 Add support for volume formatting - @adamwg ## [v1.2.0] - 2018-05-08 - #166 Remove support for Go 1.6 - @iheanyi - #165 Add support for Let's Encrypt Certificates - @viola ## [v1.1.3] - 2018-03-07 - #156 Handle non-json errors from the API - @aknuds1 - #158 Update droplet example to use latest instance type - @dan-v ## [v1.1.2] - 2018-03-06 - #157 storage: list volumes should handle only name or only region params - @andrewsykim - #154 docs: replace first example with fully-runnable example - @xmudrii - #152 Handle flags & tag properties of domain record - @jaymecd ## [v1.1.1] - 2017-09-29 - #151 Following user agent field recommendations - @joonas - #148 AsRequest method to create load balancers requests - @lukegb ## [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 godo-1.37.0/CONTRIBUTING.md000066400000000000000000000035401366527000000147640ustar00rootroot00000000000000# Contributing We love contributions! You are welcome to open a pull request, but it's a good idea to open an issue and discuss your idea with us first. Once you are ready to open a PR, please keep the following guidelines in mind: 1. Code should be `go fmt` compliant. 1. Types, structs and funcs should be documented. 1. Tests pass. ## Getting set up `godo` uses go modules. Just fork this repo, clone your fork and off you go! ## Running tests When working on code in this repository, tests can be run via: ```sh go test -mod=vendor . ``` ## Versioning Godo follows [semver](https://www.semver.org) versioning semantics. New functionality should be accompanied by increment to the minor version number. Any code merged to master is subject to release. ## Releasing Releasing a new version of godo is currently a manual process. Submit a separate pull request for the version change from the pull request with your changes. 1. Update the `CHANGELOG.md` with your changes. If a version header for the next (unreleased) version does not exist, create one. Include one bullet point for each piece of new functionality in the release, including the pull request ID, description, and author(s). ``` ## [v1.8.0] - 2019-03-13 - #210 Expose tags on storage volume create/list/get. - @jcodybaker - #123 Update test dependencies - @digitalocean ``` 2. Update the `libraryVersion` number in `godo.go`. 3. Make a pull request with these changes. This PR should be separate from the PR containing the godo changes. 4. Once the pull request has been merged, [draft a new release](https://github.com/digitalocean/godo/releases/new). 5. Update the `Tag version` and `Release title` field with the new godo version. Be sure the version has a `v` prefixed in both places. Ex `v1.8.0`. 6. Copy the changelog bullet points to the description field. 7. Publish the release. godo-1.37.0/LICENSE.txt000066400000000000000000000052061366527000000143570ustar00rootroot00000000000000Copyright (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. godo-1.37.0/README.md000066400000000000000000000071261366527000000140160ustar00rootroot00000000000000# Godo [![Build Status](https://travis-ci.org/digitalocean/godo.svg)](https://travis-ci.org/digitalocean/godo) [![GoDoc](https://godoc.org/github.com/digitalocean/godo?status.svg)](https://godoc.org/github.com/digitalocean/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/) ## Install ```sh go get github.com/digitalocean/godo@vX.Y.Z ``` where X.Y.Z is the [version](https://github.com/digitalocean/godo/releases) you need. or ```sh go get github.com/digitalocean/godo ``` for non Go modules usage or latest version. ## 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 package main import ( "github.com/digitalocean/godo" ) func main() { client := godo.NewFromToken("my-digitalocean-api-token") } ``` If you need to provide a `context.Context` to your new client, you should use [`godo.NewClient`](https://godoc.org/github.com/digitalocean/godo#NewClient) to manually construct a client instead. ## Examples To create a new Droplet: ```go dropletName := "super-cool-droplet" createRequest := &godo.DropletCreateRequest{ Name: dropletName, Region: "nyc3", Size: "s-1vcpu-1gb", 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. 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). godo-1.37.0/account.go000066400000000000000000000027741366527000000145260ustar00rootroot00000000000000package godo import ( "context" "net/http" ) // 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"` VolumeLimit int `json:"volume_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 } godo-1.37.0/account_test.go000066400000000000000000000031221366527000000155510ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" ) func TestAccountGet(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) response := ` { "account": { "droplet_limit": 25, "floating_ip_limit": 25, "volume_limit": 22, "email": "sammy@digitalocean.com", "uuid": "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", "email_verified": true } }` fmt.Fprint(w, response) }) acct, _, err := client.Account.Get(ctx) if err != nil { t.Errorf("Account.Get returned error: %v", err) } expected := &Account{DropletLimit: 25, FloatingIPLimit: 25, Email: "sammy@digitalocean.com", UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true, VolumeLimit: 22} if !reflect.DeepEqual(acct, expected) { t.Errorf("Account.Get returned %+v, expected %+v", acct, expected) } } func TestAccountString(t *testing.T) { acct := &Account{ DropletLimit: 25, FloatingIPLimit: 25, Email: "sammy@digitalocean.com", UUID: "b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified: true, Status: "active", StatusMessage: "message", VolumeLimit: 22, } stringified := acct.String() expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, VolumeLimit:22, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}` if expected != stringified { t.Errorf("Account.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/action.go000066400000000000000000000050301366527000000143330ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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) } godo-1.37.0/action_test.go000066400000000000000000000066521366527000000154050ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" "time" ) func TestAction_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{ "actions": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }, `) testMethod(t, r, http.MethodGet) }) actions, resp, err := client.Actions.List(ctx, nil) if err != nil { t.Fatalf("unexpected error: %s", err) } expectedActions := []Action{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(actions, expectedActions) { t.Errorf("Actions.List returned actions %+v, expected %+v", actions, expectedActions) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Actions.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestAction_ListActionMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"actions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/droplets/?page=2"}}}`) testMethod(t, r, http.MethodGet) }) _, resp, err := client.Actions.List(ctx, nil) if err != nil { t.Fatal(nil) } checkCurrentPage(t, resp, 1) } func TestAction_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "actions": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/actions/?page=3", "prev":"http://example.com/v2/actions/?page=1", "last":"http://example.com/v2/actions/?page=3", "first":"http://example.com/v2/actions/?page=1" } } }` mux.HandleFunc("/v2/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Actions.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestAction_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/actions/12345", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"action": {"id":12345,"region":{"name":"name","slug":"slug","available":true,"sizes":["512mb"],"features":["virtio"]},"region_slug":"slug"}}`) testMethod(t, r, http.MethodGet) }) action, _, err := client.Actions.Get(ctx, 12345) if err != nil { t.Fatalf("unexpected error: %s", err) } if action.ID != 12345 { t.Fatalf("unexpected response") } region := &Region{ Name: "name", Slug: "slug", Available: true, Sizes: []string{"512mb"}, Features: []string{"virtio"}, } if !reflect.DeepEqual(action.Region, region) { t.Fatalf("unexpected response, invalid region") } if action.RegionSlug != "slug" { t.Fatalf("unexpected response, invalid region slug") } } func TestAction_String(t *testing.T) { pt, err := time.Parse(time.RFC3339, "2014-05-08T20:36:47Z") if err != nil { t.Fatalf("unexpected error: %s", err) } startedAt := &Timestamp{ Time: pt, } action := &Action{ ID: 1, Status: "in-progress", Type: "transfer", StartedAt: startedAt, } stringified := action.String() expected := `godo.Action{ID:1, Status:"in-progress", Type:"transfer", ` + `StartedAt:godo.Timestamp{2014-05-08 20:36:47 +0000 UTC}, ` + `ResourceID:0, ResourceType:"", RegionSlug:""}` if expected != stringified { t.Errorf("Action.Stringify returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/balance.go000066400000000000000000000023621366527000000144500ustar00rootroot00000000000000package godo import ( "context" "net/http" "time" ) // BalanceService is an interface for interfacing with the Balance // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2/#balance type BalanceService interface { Get(context.Context) (*Balance, *Response, error) } // BalanceServiceOp handles communication with the Balance related methods of // the DigitalOcean API. type BalanceServiceOp struct { client *Client } var _ BalanceService = &BalanceServiceOp{} // Balance represents a DigitalOcean Balance type Balance struct { MonthToDateBalance string `json:"month_to_date_balance"` AccountBalance string `json:"account_balance"` MonthToDateUsage string `json:"month_to_date_usage"` GeneratedAt time.Time `json:"generated_at"` } func (r Balance) String() string { return Stringify(r) } // Get DigitalOcean balance info func (s *BalanceServiceOp) Get(ctx context.Context) (*Balance, *Response, error) { path := "v2/customers/my/balance" req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(Balance) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root, resp, err } godo-1.37.0/balance_test.go000066400000000000000000000015641366527000000155120ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" "time" ) func TestBalanceGet(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/balance", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) response := ` { "month_to_date_balance": "23.44", "account_balance": "12.23", "month_to_date_usage": "11.21", "generated_at": "2018-06-21T08:44:38Z" } ` fmt.Fprint(w, response) }) bal, _, err := client.Balance.Get(ctx) if err != nil { t.Errorf("Balance.Get returned error: %v", err) } expected := &Balance{ MonthToDateBalance: "23.44", AccountBalance: "12.23", MonthToDateUsage: "11.21", GeneratedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), } if !reflect.DeepEqual(bal, expected) { t.Errorf("Balance.Get returned %+v, expected %+v", bal, expected) } } godo-1.37.0/billing_history.go000066400000000000000000000036471366527000000162730ustar00rootroot00000000000000package godo import ( "context" "net/http" "time" ) const billingHistoryBasePath = "v2/customers/my/billing_history" // BillingHistoryService is an interface for interfacing with the BillingHistory // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2/#billing_history type BillingHistoryService interface { List(context.Context, *ListOptions) (*BillingHistory, *Response, error) } // BillingHistoryServiceOp handles communication with the BillingHistory related methods of // the DigitalOcean API. type BillingHistoryServiceOp struct { client *Client } var _ BillingHistoryService = &BillingHistoryServiceOp{} // BillingHistory represents a DigitalOcean Billing History type BillingHistory struct { BillingHistory []BillingHistoryEntry `json:"billing_history"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // BillingHistoryEntry represents an entry in a customer's Billing History type BillingHistoryEntry struct { Description string `json:"description"` Amount string `json:"amount"` InvoiceID *string `json:"invoice_id"` InvoiceUUID *string `json:"invoice_uuid"` Date time.Time `json:"date"` Type string `json:"type"` } func (b BillingHistory) String() string { return Stringify(b) } // List the Billing History for a customer func (s *BillingHistoryServiceOp) List(ctx context.Context, opt *ListOptions) (*BillingHistory, *Response, error) { path, err := addOptions(billingHistoryBasePath, 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(BillingHistory) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root, resp, err } godo-1.37.0/billing_history_test.go000066400000000000000000000032661366527000000173270ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" "time" ) func TestBillingHistory_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/billing_history", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "billing_history": [ { "description": "Invoice for May 2018", "amount": "12.34", "invoice_id": "123", "invoice_uuid": "example-uuid", "date": "2018-06-01T08:44:38Z", "type": "Invoice" }, { "description": "Payment (MC 2018)", "amount": "-12.34", "date": "2018-06-02T08:44:38Z", "type": "Payment" } ], "meta": { "total": 2 } }`) }) history, resp, err := client.BillingHistory.List(ctx, nil) if err != nil { t.Errorf("BillingHistory.List returned error: %v", err) } expectedBillingHistory := []BillingHistoryEntry{ { Description: "Invoice for May 2018", Amount: "12.34", InvoiceID: String("123"), InvoiceUUID: String("example-uuid"), Date: time.Date(2018, 6, 1, 8, 44, 38, 0, time.UTC), Type: "Invoice", }, { Description: "Payment (MC 2018)", Amount: "-12.34", InvoiceID: nil, InvoiceUUID: nil, Date: time.Date(2018, 6, 2, 8, 44, 38, 0, time.UTC), Type: "Payment", }, } entries := history.BillingHistory if !reflect.DeepEqual(entries, expectedBillingHistory) { t.Errorf("BillingHistory.List\nBillingHistory: got=%#v\nwant=%#v", entries, expectedBillingHistory) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("BillingHistory.List\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } godo-1.37.0/cdn.go000066400000000000000000000136531366527000000136340ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" "time" ) const cdnBasePath = "v2/cdn/endpoints" // CDNService is an interface for managing Spaces CDN with the DigitalOcean API. type CDNService interface { List(context.Context, *ListOptions) ([]CDN, *Response, error) Get(context.Context, string) (*CDN, *Response, error) Create(context.Context, *CDNCreateRequest) (*CDN, *Response, error) UpdateTTL(context.Context, string, *CDNUpdateTTLRequest) (*CDN, *Response, error) UpdateCustomDomain(context.Context, string, *CDNUpdateCustomDomainRequest) (*CDN, *Response, error) FlushCache(context.Context, string, *CDNFlushCacheRequest) (*Response, error) Delete(context.Context, string) (*Response, error) } // CDNServiceOp handles communication with the CDN related methods of the // DigitalOcean API. type CDNServiceOp struct { client *Client } var _ CDNService = &CDNServiceOp{} // CDN represents a DigitalOcean CDN type CDN struct { ID string `json:"id"` Origin string `json:"origin"` Endpoint string `json:"endpoint"` CreatedAt time.Time `json:"created_at"` TTL uint32 `json:"ttl"` CertificateID string `json:"certificate_id,omitempty"` CustomDomain string `json:"custom_domain,omitempty"` } // CDNRoot represents a response from the DigitalOcean API type cdnRoot struct { Endpoint *CDN `json:"endpoint"` } type cdnsRoot struct { Endpoints []CDN `json:"endpoints"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // CDNCreateRequest represents a request to create a CDN. type CDNCreateRequest struct { Origin string `json:"origin"` TTL uint32 `json:"ttl"` CustomDomain string `json:"custom_domain,omitempty"` CertificateID string `json:"certificate_id,omitempty"` } // CDNUpdateTTLRequest represents a request to update the ttl of a CDN. type CDNUpdateTTLRequest struct { TTL uint32 `json:"ttl"` } // CDNUpdateCustomDomainRequest represents a request to update the custom domain of a CDN. type CDNUpdateCustomDomainRequest struct { CustomDomain string `json:"custom_domain"` CertificateID string `json:"certificate_id"` } // CDNFlushCacheRequest represents a request to flush cache of a CDN. type CDNFlushCacheRequest struct { Files []string `json:"files"` } // List all CDN endpoints func (c CDNServiceOp) List(ctx context.Context, opt *ListOptions) ([]CDN, *Response, error) { path, err := addOptions(cdnBasePath, opt) if err != nil { return nil, nil, err } req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(cdnsRoot) resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Endpoints, resp, err } // Get individual CDN. It requires a non-empty cdn id. func (c CDNServiceOp) Get(ctx context.Context, id string) (*CDN, *Response, error) { if len(id) == 0 { return nil, nil, NewArgError("id", "cannot be an empty string") } path := fmt.Sprintf("%s/%s", cdnBasePath, id) req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(cdnRoot) resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Endpoint, resp, err } // Create a new CDN func (c CDNServiceOp) Create(ctx context.Context, createRequest *CDNCreateRequest) (*CDN, *Response, error) { if createRequest == nil { return nil, nil, NewArgError("createRequest", "cannot be nil") } req, err := c.client.NewRequest(ctx, http.MethodPost, cdnBasePath, createRequest) if err != nil { return nil, nil, err } root := new(cdnRoot) resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Endpoint, resp, err } // UpdateTTL updates the ttl of an individual CDN func (c CDNServiceOp) UpdateTTL(ctx context.Context, id string, updateRequest *CDNUpdateTTLRequest) (*CDN, *Response, error) { return c.update(ctx, id, updateRequest) } // UpdateCustomDomain sets or removes the custom domain of an individual CDN func (c CDNServiceOp) UpdateCustomDomain(ctx context.Context, id string, updateRequest *CDNUpdateCustomDomainRequest) (*CDN, *Response, error) { return c.update(ctx, id, updateRequest) } func (c CDNServiceOp) update(ctx context.Context, id string, updateRequest interface{}) (*CDN, *Response, error) { if updateRequest == nil { return nil, nil, NewArgError("updateRequest", "cannot be nil") } if len(id) == 0 { return nil, nil, NewArgError("id", "cannot be an empty string") } path := fmt.Sprintf("%s/%s", cdnBasePath, id) req, err := c.client.NewRequest(ctx, http.MethodPut, path, updateRequest) if err != nil { return nil, nil, err } root := new(cdnRoot) resp, err := c.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Endpoint, resp, err } // FlushCache flushes the cache of an individual CDN. Requires a non-empty slice of file paths and/or wildcards func (c CDNServiceOp) FlushCache(ctx context.Context, id string, flushCacheRequest *CDNFlushCacheRequest) (*Response, error) { if flushCacheRequest == nil { return nil, NewArgError("flushCacheRequest", "cannot be nil") } if len(id) == 0 { return nil, NewArgError("id", "cannot be an empty string") } path := fmt.Sprintf("%s/%s/cache", cdnBasePath, id) req, err := c.client.NewRequest(ctx, http.MethodDelete, path, flushCacheRequest) if err != nil { return nil, err } resp, err := c.client.Do(ctx, req, nil) return resp, err } // Delete an individual CDN func (c CDNServiceOp) Delete(ctx context.Context, id string) (*Response, error) { if len(id) == 0 { return nil, NewArgError("id", "cannot be an empty string") } path := fmt.Sprintf("%s/%s", cdnBasePath, id) req, err := c.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := c.client.Do(ctx, req, nil) return resp, err } godo-1.37.0/cdn_test.go000066400000000000000000000272131366527000000146700ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" "time" ) func TestCDN_ListCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint( w, `{ "endpoints": [ { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z" }, { "id": "892071a0-bb95-55bd-8021-3afd67a210bf", "origin": "my-space1.nyc3.digitaloceanspaces.com", "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-03T15:00:01.05Z" } ], "meta": { "total": 2 } }`, ) }) cdns, resp, err := client.CDNs.List(ctx, nil) if err != nil { t.Errorf("CDNs.List returned error: %v", err) } expectedCDNs := []CDN{ { ID: "892071a0-bb95-49bc-8021-3afd67a210bf", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 3600, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), }, { ID: "892071a0-bb95-55bd-8021-3afd67a210bf", Origin: "my-space1.nyc3.digitaloceanspaces.com", Endpoint: "my-space1.nyc3.cdn.digitaloceanspaces.com", TTL: 3600, CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC), }, } if !reflect.DeepEqual(cdns, expectedCDNs) { t.Errorf("CDNs.List returned CDNs %+v, expected %+v", cdns, expectedCDNs) } expectedMeta := &Meta{ Total: 2, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("CDNs.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestCDN_ListCDNMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint( w, `{ "endpoints": [ { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z" }, { "id": "892071a0-bb95-55bd-8021-3afd67a210bf", "origin": "my-space1.nyc3.digitaloceanspaces.com", "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-03T15:00:01.05Z" } ], "links":{"pages":{"next":"http://example.com/v2/cdn/endpoints/?page=2"}} }`, ) }) _, resp, err := client.CDNs.List(ctx, nil) if err != nil { t.Errorf("CDNs.List multiple page returned error: %v", err) } checkCurrentPage(t, resp, 1) } func TestCDN_RetrievePageByNumber(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint( w, `{ "endpoints": [ { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z" }, { "id": "892071a0-bb95-55bd-8021-3afd67a210bf", "origin": "my-space1.nyc3.digitaloceanspaces.com", "endpoint": "my-space1.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-03T15:00:01.05Z" } ], "links":{"pages":{ "next":"http://example.com/v2/cdn/endpoints/?page=3", "prev":"http://example.com/v2/cdn/endpoints/?page=1", "last":"http://example.com/v2/cdn/endpoints/?page=3", "first":"http://example.com/v2/cdn/endpoints/?page=1"}} }`, ) }) _, resp, err := client.CDNs.List(ctx, &ListOptions{Page: 2}) if err != nil { t.Errorf("CDNs.List singular page returned error: %v", err) } checkCurrentPage(t, resp, 2) } func TestCDN_GetCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z" } }`, ) }) cdn, _, err := client.CDNs.Get(ctx, "12345") if err != nil { t.Errorf("CDNs.Get returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 3600, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.Get returned %+v, expected %+v", cdn, expected) } } func TestCDN_CreateCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z" } }`, ) }) req := &CDNCreateRequest{Origin: "my-space.nyc3.digitaloceanspaces.com", TTL: 3600} cdn, _, err := client.CDNs.Create(ctx, req) if err != nil { t.Errorf("CDNs.Create returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 3600, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.Create returned %+v, expected %+v", cdn, expected) } } func TestCDN_CreateCustomDomainCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 3600, "created_at": "2012-10-02T15:00:01.05Z", "custom_domain": "assets.myacmecorp.com", "certificate_id": "a20489cc-d278-48d2-8e10-45d42a312451" } }`, ) }) req := &CDNCreateRequest{ Origin: "my-space.nyc3.digitaloceanspaces.com", TTL: 3600, CustomDomain: "assets.myacmecorp.com", CertificateID: "a20489cc-d278-48d2-8e10-45d42a312451", } cdn, _, err := client.CDNs.Create(ctx, req) if err != nil { t.Errorf("CDNs.Create returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 3600, CustomDomain: "assets.myacmecorp.com", CertificateID: "a20489cc-d278-48d2-8e10-45d42a312451", CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.Create returned %+v, expected %+v", cdn, expected) } } func TestCDN_DeleteCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.CDNs.Delete(ctx, "12345") if err != nil { t.Errorf("CDNs.Delete returned error: %v", err) } } func TestCDN_UpdateTTLCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 60, "created_at": "2012-10-02T15:00:01.05Z" } }`, ) }) req := &CDNUpdateTTLRequest{TTL: 60} cdn, _, err := client.CDNs.UpdateTTL(ctx, "12345", req) if err != nil { t.Errorf("CDNs.UpdateTTL returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 60, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.UpdateTTL returned %+v, expected %+v", cdn, expected) } } func TestCDN_UpdateAddCustomDomainCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 60, "created_at": "2012-10-02T15:00:01.05Z", "custom_domain": "assets.myacmecorp.com", "certificate_id": "a20489cc-d278-48d2-8e10-45d42a312451" } }`, ) }) req := &CDNUpdateCustomDomainRequest{ CustomDomain: "assets.myacmecorp.com", CertificateID: "a20489cc-d278-48d2-8e10-45d42a312451", } cdn, _, err := client.CDNs.UpdateCustomDomain(ctx, "12345", req) if err != nil { t.Errorf("CDNs.UpdateTTL returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", CustomDomain: "assets.myacmecorp.com", CertificateID: "a20489cc-d278-48d2-8e10-45d42a312451", TTL: 60, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.UpdateTTL returned %+v, expected %+v", cdn, expected) } } func TestCDN_UpdateRemoveCustomDomainCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) fmt.Fprint( w, `{ "endpoint": { "id": "12345", "origin": "my-space.nyc3.digitaloceanspaces.com", "endpoint": "my-space.nyc3.cdn.digitaloceanspaces.com", "ttl": 60, "created_at": "2012-10-02T15:00:01.05Z" } }`, ) }) req := &CDNUpdateCustomDomainRequest{} cdn, _, err := client.CDNs.UpdateCustomDomain(ctx, "12345", req) if err != nil { t.Errorf("CDNs.UpdateTTL returned error: %v", err) } expected := &CDN{ ID: "12345", Origin: "my-space.nyc3.digitaloceanspaces.com", Endpoint: "my-space.nyc3.cdn.digitaloceanspaces.com", TTL: 60, CreatedAt: time.Date(2012, 10, 02, 15, 00, 01, 50000000, time.UTC), } if !reflect.DeepEqual(cdn, expected) { t.Errorf("CDNs.UpdateTTL returned %+v, expected %+v", cdn, expected) } } func TestCDN_FluchCacheCDN(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/cdn/endpoints/12345/cache", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) req := &CDNFlushCacheRequest{Files: []string{"*"}} _, err := client.CDNs.FlushCache(ctx, "12345", req) if err != nil { t.Errorf("CDNs.FlushCache returned error: %v", err) } } godo-1.37.0/certificates.go000066400000000000000000000074401366527000000155320ustar00rootroot00000000000000package godo import ( "context" "net/http" "path" ) const certificatesBasePath = "/v2/certificates" // CertificatesService is an interface for managing certificates with the DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2/#certificates type CertificatesService interface { Get(context.Context, string) (*Certificate, *Response, error) List(context.Context, *ListOptions) ([]Certificate, *Response, error) Create(context.Context, *CertificateRequest) (*Certificate, *Response, error) Delete(context.Context, string) (*Response, error) } // Certificate represents a DigitalOcean certificate configuration. type Certificate struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` DNSNames []string `json:"dns_names,omitempty"` NotAfter string `json:"not_after,omitempty"` SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"` Created string `json:"created_at,omitempty"` State string `json:"state,omitempty"` Type string `json:"type,omitempty"` } // CertificateRequest represents configuration for a new certificate. type CertificateRequest struct { Name string `json:"name,omitempty"` DNSNames []string `json:"dns_names,omitempty"` PrivateKey string `json:"private_key,omitempty"` LeafCertificate string `json:"leaf_certificate,omitempty"` CertificateChain string `json:"certificate_chain,omitempty"` Type string `json:"type,omitempty"` } type certificateRoot struct { Certificate *Certificate `json:"certificate"` } type certificatesRoot struct { Certificates []Certificate `json:"certificates"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // 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 } if m := root.Meta; m != nil { resp.Meta = m } 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) } godo-1.37.0/certificates_test.go000066400000000000000000000154711366527000000165740ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "path" "testing" "github.com/stretchr/testify/assert" ) var certJSONResponse = ` { "certificate": { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "name": "web-cert-01", "dns_names": [ "somedomain.com", "api.somedomain.com" ], "not_after": "2017-02-22T00:23:00Z", "sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", "created_at": "2017-02-08T16:02:37Z", "state": "verified", "type": "custom" } } ` var certsJSONResponse = ` { "certificates": [ { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "name": "web-cert-01", "dns_names": [ "somedomain.com", "api.somedomain.com" ], "not_after": "2017-02-22T00:23:00Z", "sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", "created_at": "2017-02-08T16:02:37Z", "state": "verified", "type": "custom" }, { "id": "992071a0-bb95-49bc-8021-3afd67a210bf", "name": "web-cert-02", "dns_names":["example.com"], "not_after": "2017-02-22T00:23:00Z", "sha1_fingerprint": "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7", "created_at": "2017-02-08T16:02:37Z", "state": "pending", "type": "lets_encrypt" } ], "links": {}, "meta": { "total": 2 } } ` func TestCertificates_Get(t *testing.T) { setup() defer teardown() urlStr := "/v2/certificates" cID := "892071a0-bb95-49bc-8021-3afd67a210bf" urlStr = path.Join(urlStr, cID) mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, certJSONResponse) }) certificate, _, err := client.Certificates.Get(ctx, cID) if err != nil { t.Errorf("Certificates.Get returned error: %v", err) } expected := &Certificate{ ID: "892071a0-bb95-49bc-8021-3afd67a210bf", Name: "web-cert-01", DNSNames: []string{"somedomain.com", "api.somedomain.com"}, NotAfter: "2017-02-22T00:23:00Z", SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", Created: "2017-02-08T16:02:37Z", State: "verified", Type: "custom", } assert.Equal(t, expected, certificate) } func TestCertificates_List(t *testing.T) { setup() defer teardown() urlStr := "/v2/certificates" mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, certsJSONResponse) }) certificates, resp, err := client.Certificates.List(ctx, nil) if err != nil { t.Errorf("Certificates.List returned error: %v", err) } expectedCertificates := []Certificate{ { ID: "892071a0-bb95-49bc-8021-3afd67a210bf", Name: "web-cert-01", DNSNames: []string{"somedomain.com", "api.somedomain.com"}, NotAfter: "2017-02-22T00:23:00Z", SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", Created: "2017-02-08T16:02:37Z", State: "verified", Type: "custom", }, { ID: "992071a0-bb95-49bc-8021-3afd67a210bf", Name: "web-cert-02", DNSNames: []string{"example.com"}, NotAfter: "2017-02-22T00:23:00Z", SHA1Fingerprint: "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7", Created: "2017-02-08T16:02:37Z", State: "pending", Type: "lets_encrypt", }, } assert.Equal(t, expectedCertificates, certificates) expectedMeta := &Meta{ Total: 2, } assert.Equal(t, expectedMeta, resp.Meta) } func TestCertificates_Create(t *testing.T) { tests := []struct { desc string createRequest *CertificateRequest certJSONResponse string expectedCertificate *Certificate }{ { desc: "creates custom certificate", createRequest: &CertificateRequest{ Name: "web-cert-01", PrivateKey: "-----BEGIN PRIVATE KEY-----", LeafCertificate: "-----BEGIN CERTIFICATE-----", CertificateChain: "-----BEGIN CERTIFICATE-----", }, certJSONResponse: `{ "certificate": { "id": "892071a0-bb95-49bc-8021-3afd67a210bf", "name": "custom-cert", "dns_names":[], "not_after": "2017-02-22T00:23:00Z", "sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", "created_at": "2017-02-08T16:02:37Z", "state": "verified", "type": "custom" } }`, expectedCertificate: &Certificate{ ID: "892071a0-bb95-49bc-8021-3afd67a210bf", Name: "custom-cert", DNSNames: []string{}, NotAfter: "2017-02-22T00:23:00Z", SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7", Created: "2017-02-08T16:02:37Z", State: "verified", Type: "custom", }, }, { desc: "creates let's encrypt certificate", createRequest: &CertificateRequest{ Name: "lets-encrypt-cert", DNSNames: []string{"example.com", "api.example.com"}, Type: "lets_encrypt", }, certJSONResponse: `{ "certificate": { "id": "91bce928-a983-4c97-a5ee-78c585bf798d", "name": "lets-encrypt-cert", "dns_names":["example.com", "api.example.com"], "not_after": "2022-01-26T15:50:00Z", "sha1_fingerprint": "2e3c2ba8016faf80f431700ff2865ef6dba30a81", "created_at": "2017-08-23T20:42:46Z", "state": "pending", "type": "lets_encrypt" } }`, expectedCertificate: &Certificate{ ID: "91bce928-a983-4c97-a5ee-78c585bf798d", Name: "lets-encrypt-cert", DNSNames: []string{"example.com", "api.example.com"}, NotAfter: "2022-01-26T15:50:00Z", SHA1Fingerprint: "2e3c2ba8016faf80f431700ff2865ef6dba30a81", Created: "2017-08-23T20:42:46Z", State: "pending", Type: "lets_encrypt", }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { setup() defer teardown() urlStr := "/v2/certificates" mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(CertificateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) assert.Equal(t, test.createRequest, v) fmt.Fprint(w, test.certJSONResponse) }) certificate, _, err := client.Certificates.Create(ctx, test.createRequest) if err != nil { t.Errorf("Certificates.Create returned error: %v", err) } assert.Equal(t, test.expectedCertificate, certificate) }) } } func TestCertificates_Delete(t *testing.T) { setup() defer teardown() cID := "892071a0-bb95-49bc-8021-3afd67a210bf" urlStr := "/v2/certificates" urlStr = path.Join(urlStr, cID) mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Certificates.Delete(ctx, cID) if err != nil { t.Errorf("Certificates.Delete returned error: %v", err) } } godo-1.37.0/databases.go000066400000000000000000000746251366527000000150250ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" "strings" "time" ) const ( databaseBasePath = "/v2/databases" databaseSinglePath = databaseBasePath + "/%s" databaseResizePath = databaseBasePath + "/%s/resize" databaseMigratePath = databaseBasePath + "/%s/migrate" databaseMaintenancePath = databaseBasePath + "/%s/maintenance" databaseBackupsPath = databaseBasePath + "/%s/backups" databaseUsersPath = databaseBasePath + "/%s/users" databaseUserPath = databaseBasePath + "/%s/users/%s" databaseResetUserAuthPath = databaseUserPath + "/reset_auth" databaseDBPath = databaseBasePath + "/%s/dbs/%s" databaseDBsPath = databaseBasePath + "/%s/dbs" databasePoolPath = databaseBasePath + "/%s/pools/%s" databasePoolsPath = databaseBasePath + "/%s/pools" databaseReplicaPath = databaseBasePath + "/%s/replicas/%s" databaseReplicasPath = databaseBasePath + "/%s/replicas" databaseEvictionPolicyPath = databaseBasePath + "/%s/eviction_policy" databaseSQLModePath = databaseBasePath + "/%s/sql_mode" databaseFirewallRulesPath = databaseBasePath + "/%s/firewall" ) // SQL Mode constants allow for MySQL-specific SQL flavor configuration. const ( SQLModeAllowInvalidDates = "ALLOW_INVALID_DATES" SQLModeANSIQuotes = "ANSI_QUOTES" SQLModeHighNotPrecedence = "HIGH_NOT_PRECEDENCE" SQLModeIgnoreSpace = "IGNORE_SPACE" SQLModeNoAuthCreateUser = "NO_AUTO_CREATE_USER" SQLModeNoAutoValueOnZero = "NO_AUTO_VALUE_ON_ZERO" SQLModeNoBackslashEscapes = "NO_BACKSLASH_ESCAPES" SQLModeNoDirInCreate = "NO_DIR_IN_CREATE" SQLModeNoEngineSubstitution = "NO_ENGINE_SUBSTITUTION" SQLModeNoFieldOptions = "NO_FIELD_OPTIONS" SQLModeNoKeyOptions = "NO_KEY_OPTIONS" SQLModeNoTableOptions = "NO_TABLE_OPTIONS" SQLModeNoUnsignedSubtraction = "NO_UNSIGNED_SUBTRACTION" SQLModeNoZeroDate = "NO_ZERO_DATE" SQLModeNoZeroInDate = "NO_ZERO_IN_DATE" SQLModeOnlyFullGroupBy = "ONLY_FULL_GROUP_BY" SQLModePadCharToFullLength = "PAD_CHAR_TO_FULL_LENGTH" SQLModePipesAsConcat = "PIPES_AS_CONCAT" SQLModeRealAsFloat = "REAL_AS_FLOAT" SQLModeStrictAllTables = "STRICT_ALL_TABLES" SQLModeStrictTransTables = "STRICT_TRANS_TABLES" SQLModeANSI = "ANSI" SQLModeDB2 = "DB2" SQLModeMaxDB = "MAXDB" SQLModeMSSQL = "MSSQL" SQLModeMYSQL323 = "MYSQL323" SQLModeMYSQL40 = "MYSQL40" SQLModeOracle = "ORACLE" SQLModePostgreSQL = "POSTGRESQL" SQLModeTraditional = "TRADITIONAL" ) // SQL Auth constants allow for MySQL-specific user auth plugins const ( SQLAuthPluginNative = "mysql_native_password" SQLAuthPluginCachingSHA2 = "caching_sha2_password" ) // Redis eviction policies supported by the managed Redis product. const ( EvictionPolicyNoEviction = "noeviction" EvictionPolicyAllKeysLRU = "allkeys_lru" EvictionPolicyAllKeysRandom = "allkeys_random" EvictionPolicyVolatileLRU = "volatile_lru" EvictionPolicyVolatileRandom = "volatile_random" EvictionPolicyVolatileTTL = "volatile_ttl" ) // The DatabasesService provides access to the DigitalOcean managed database // suite of products through the public API. Customers can create new database // clusters, migrate them between regions, create replicas and interact with // their configurations. Each database service is refered to as a Database. A // SQL database service can have multiple databases residing in the system. To // help make these entities distinct from Databases in godo, we refer to them // here as DatabaseDBs. // // See: https://developers.digitalocean.com/documentation/v2#databases type DatabasesService interface { List(context.Context, *ListOptions) ([]Database, *Response, error) Get(context.Context, string) (*Database, *Response, error) Create(context.Context, *DatabaseCreateRequest) (*Database, *Response, error) Delete(context.Context, string) (*Response, error) Resize(context.Context, string, *DatabaseResizeRequest) (*Response, error) Migrate(context.Context, string, *DatabaseMigrateRequest) (*Response, error) UpdateMaintenance(context.Context, string, *DatabaseUpdateMaintenanceRequest) (*Response, error) ListBackups(context.Context, string, *ListOptions) ([]DatabaseBackup, *Response, error) GetUser(context.Context, string, string) (*DatabaseUser, *Response, error) ListUsers(context.Context, string, *ListOptions) ([]DatabaseUser, *Response, error) CreateUser(context.Context, string, *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error) DeleteUser(context.Context, string, string) (*Response, error) ResetUserAuth(context.Context, string, string, *DatabaseResetUserAuthRequest) (*DatabaseUser, *Response, error) ListDBs(context.Context, string, *ListOptions) ([]DatabaseDB, *Response, error) CreateDB(context.Context, string, *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error) GetDB(context.Context, string, string) (*DatabaseDB, *Response, error) DeleteDB(context.Context, string, string) (*Response, error) ListPools(context.Context, string, *ListOptions) ([]DatabasePool, *Response, error) CreatePool(context.Context, string, *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error) GetPool(context.Context, string, string) (*DatabasePool, *Response, error) DeletePool(context.Context, string, string) (*Response, error) GetReplica(context.Context, string, string) (*DatabaseReplica, *Response, error) ListReplicas(context.Context, string, *ListOptions) ([]DatabaseReplica, *Response, error) CreateReplica(context.Context, string, *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error) DeleteReplica(context.Context, string, string) (*Response, error) GetEvictionPolicy(context.Context, string) (string, *Response, error) SetEvictionPolicy(context.Context, string, string) (*Response, error) GetSQLMode(context.Context, string) (string, *Response, error) SetSQLMode(context.Context, string, ...string) (*Response, error) GetFirewallRules(context.Context, string) ([]DatabaseFirewallRule, *Response, error) UpdateFirewallRules(context.Context, string, *DatabaseUpdateFirewallRulesRequest) (*Response, error) } // DatabasesServiceOp handles communication with the Databases related methods // of the DigitalOcean API. type DatabasesServiceOp struct { client *Client } var _ DatabasesService = &DatabasesServiceOp{} // Database represents a DigitalOcean managed database product. These managed databases // are usually comprised of a cluster of database nodes, a primary and 0 or more replicas. // The EngineSlug is a string which indicates the type of database service. Some examples are // "pg", "mysql" or "redis". A Database also includes connection information and other // properties of the service like region, size and current status. type Database struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` EngineSlug string `json:"engine,omitempty"` VersionSlug string `json:"version,omitempty"` Connection *DatabaseConnection `json:"connection,omitempty"` PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"` Users []DatabaseUser `json:"users,omitempty"` NumNodes int `json:"num_nodes,omitempty"` SizeSlug string `json:"size,omitempty"` DBNames []string `json:"db_names,omitempty"` RegionSlug string `json:"region,omitempty"` Status string `json:"status,omitempty"` MaintenanceWindow *DatabaseMaintenanceWindow `json:"maintenance_window,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` PrivateNetworkUUID string `json:"private_network_uuid,omitempty"` Tags []string `json:"tags,omitempty"` } // DatabaseConnection represents a database connection type DatabaseConnection struct { URI string `json:"uri,omitempty"` Database string `json:"database,omitempty"` Host string `json:"host,omitempty"` Port int `json:"port,omitempty"` User string `json:"user,omitempty"` Password string `json:"password,omitempty"` SSL bool `json:"ssl,omitempty"` } // DatabaseUser represents a user in the database type DatabaseUser struct { Name string `json:"name,omitempty"` Role string `json:"role,omitempty"` Password string `json:"password,omitempty"` MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"` } // DatabaseMySQLUserSettings contains MySQL-specific user settings type DatabaseMySQLUserSettings struct { AuthPlugin string `json:"auth_plugin"` } // DatabaseMaintenanceWindow represents the maintenance_window of a database // cluster type DatabaseMaintenanceWindow struct { Day string `json:"day,omitempty"` Hour string `json:"hour,omitempty"` Pending bool `json:"pending,omitempty"` Description []string `json:"description,omitempty"` } // DatabaseBackup represents a database backup. type DatabaseBackup struct { CreatedAt time.Time `json:"created_at,omitempty"` SizeGigabytes float64 `json:"size_gigabytes,omitempty"` } // DatabaseCreateRequest represents a request to create a database cluster type DatabaseCreateRequest struct { Name string `json:"name,omitempty"` EngineSlug string `json:"engine,omitempty"` Version string `json:"version,omitempty"` SizeSlug string `json:"size,omitempty"` Region string `json:"region,omitempty"` NumNodes int `json:"num_nodes,omitempty"` PrivateNetworkUUID string `json:"private_network_uuid"` Tags []string `json:"tags,omitempty"` } // DatabaseResizeRequest can be used to initiate a database resize operation. type DatabaseResizeRequest struct { SizeSlug string `json:"size,omitempty"` NumNodes int `json:"num_nodes,omitempty"` } // DatabaseMigrateRequest can be used to initiate a database migrate operation. type DatabaseMigrateRequest struct { Region string `json:"region,omitempty"` PrivateNetworkUUID string `json:"private_network_uuid"` } // DatabaseUpdateMaintenanceRequest can be used to update the database's maintenance window. type DatabaseUpdateMaintenanceRequest struct { Day string `json:"day,omitempty"` Hour string `json:"hour,omitempty"` } // DatabaseDB represents an engine-specific database created within a database cluster. For SQL // databases like PostgreSQL or MySQL, a "DB" refers to a database created on the RDBMS. For instance, // a PostgreSQL database server can contain many database schemas, each with it's own settings, access // permissions and data. ListDBs will return all databases present on the server. type DatabaseDB struct { Name string `json:"name"` } // DatabaseReplica represents a read-only replica of a particular database type DatabaseReplica struct { Name string `json:"name"` Connection *DatabaseConnection `json:"connection"` PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"` Region string `json:"region"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` PrivateNetworkUUID string `json:"private_network_uuid,omitempty"` Tags []string `json:"tags,omitempty"` } // DatabasePool represents a database connection pool type DatabasePool struct { User string `json:"user"` Name string `json:"name"` Size int `json:"size"` Database string `json:"db"` Mode string `json:"mode"` Connection *DatabaseConnection `json:"connection"` PrivateConnection *DatabaseConnection `json:"private_connection,omitempty"` } // DatabaseCreatePoolRequest is used to create a new database connection pool type DatabaseCreatePoolRequest struct { User string `json:"user"` Name string `json:"name"` Size int `json:"size"` Database string `json:"db"` Mode string `json:"mode"` } // DatabaseCreateUserRequest is used to create a new database user type DatabaseCreateUserRequest struct { Name string `json:"name"` MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"` } // DatabaseResetUserAuth request is used to reset a users DB auth type DatabaseResetUserAuthRequest struct { MySQLSettings *DatabaseMySQLUserSettings `json:"mysql_settings,omitempty"` } // DatabaseCreateDBRequest is used to create a new engine-specific database within the cluster type DatabaseCreateDBRequest struct { Name string `json:"name"` } // DatabaseCreateReplicaRequest is used to create a new read-only replica type DatabaseCreateReplicaRequest struct { Name string `json:"name"` Region string `json:"region"` Size string `json:"size"` PrivateNetworkUUID string `json:"private_network_uuid"` Tags []string `json:"tags,omitempty"` } // DatabaseUpdateFirewallRulesRequest is used to set the firewall rules for a database type DatabaseUpdateFirewallRulesRequest struct { Rules []*DatabaseFirewallRule `json:"rules"` } // DatabaseFirewallRule is a rule describing an inbound source to a database type DatabaseFirewallRule struct { UUID string `json:"uuid"` ClusterUUID string `json:"cluster_uuid"` Type string `json:"type"` Value string `json:"value"` CreatedAt time.Time `json:"created_at"` } type databaseUserRoot struct { User *DatabaseUser `json:"user"` } type databaseUsersRoot struct { Users []DatabaseUser `json:"users"` } type databaseDBRoot struct { DB *DatabaseDB `json:"db"` } type databaseDBsRoot struct { DBs []DatabaseDB `json:"dbs"` } type databasesRoot struct { Databases []Database `json:"databases"` } type databaseRoot struct { Database *Database `json:"database"` } type databaseBackupsRoot struct { Backups []DatabaseBackup `json:"backups"` } type databasePoolRoot struct { Pool *DatabasePool `json:"pool"` } type databasePoolsRoot struct { Pools []DatabasePool `json:"pools"` } type databaseReplicaRoot struct { Replica *DatabaseReplica `json:"replica"` } type databaseReplicasRoot struct { Replicas []DatabaseReplica `json:"replicas"` } type evictionPolicyRoot struct { EvictionPolicy string `json:"eviction_policy"` } type sqlModeRoot struct { SQLMode string `json:"sql_mode"` } type databaseFirewallRuleRoot struct { Rules []DatabaseFirewallRule `json:"rules"` } // URN returns a URN identifier for the database func (d Database) URN() string { return ToURN("dbaas", d.ID) } // List returns a list of the Databases visible with the caller's API token func (svc *DatabasesServiceOp) List(ctx context.Context, opts *ListOptions) ([]Database, *Response, error) { path := databaseBasePath path, err := addOptions(path, opts) 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(databasesRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Databases, resp, nil } // Get retrieves the details of a database cluster func (svc *DatabasesServiceOp) Get(ctx context.Context, databaseID string) (*Database, *Response, error) { path := fmt.Sprintf(databaseSinglePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(databaseRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Database, resp, nil } // Create creates a database cluster func (svc *DatabasesServiceOp) Create(ctx context.Context, create *DatabaseCreateRequest) (*Database, *Response, error) { path := databaseBasePath req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create) if err != nil { return nil, nil, err } root := new(databaseRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Database, resp, nil } // Delete deletes a database cluster. There is no way to recover a cluster once // it has been destroyed. func (svc *DatabasesServiceOp) Delete(ctx context.Context, databaseID string) (*Response, error) { path := fmt.Sprintf("%s/%s", databaseBasePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // Resize resizes a database cluster by number of nodes or size func (svc *DatabasesServiceOp) Resize(ctx context.Context, databaseID string, resize *DatabaseResizeRequest) (*Response, error) { path := fmt.Sprintf(databaseResizePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, resize) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // Migrate migrates a database cluster to a new region func (svc *DatabasesServiceOp) Migrate(ctx context.Context, databaseID string, migrate *DatabaseMigrateRequest) (*Response, error) { path := fmt.Sprintf(databaseMigratePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, migrate) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // UpdateMaintenance updates the maintenance window on a cluster func (svc *DatabasesServiceOp) UpdateMaintenance(ctx context.Context, databaseID string, maintenance *DatabaseUpdateMaintenanceRequest) (*Response, error) { path := fmt.Sprintf(databaseMaintenancePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, maintenance) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // ListBackups returns a list of the current backups of a database func (svc *DatabasesServiceOp) ListBackups(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseBackup, *Response, error) { path := fmt.Sprintf(databaseBackupsPath, databaseID) path, err := addOptions(path, opts) 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(databaseBackupsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Backups, resp, nil } // GetUser returns the database user identified by userID func (svc *DatabasesServiceOp) GetUser(ctx context.Context, databaseID, userID string) (*DatabaseUser, *Response, error) { path := fmt.Sprintf(databaseUserPath, databaseID, userID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(databaseUserRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.User, resp, nil } // ListUsers returns all database users for the database func (svc *DatabasesServiceOp) ListUsers(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseUser, *Response, error) { path := fmt.Sprintf(databaseUsersPath, databaseID) path, err := addOptions(path, opts) 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(databaseUsersRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Users, resp, nil } // CreateUser will create a new database user func (svc *DatabasesServiceOp) CreateUser(ctx context.Context, databaseID string, createUser *DatabaseCreateUserRequest) (*DatabaseUser, *Response, error) { path := fmt.Sprintf(databaseUsersPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createUser) if err != nil { return nil, nil, err } root := new(databaseUserRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.User, resp, nil } func (svc *DatabasesServiceOp) ResetUserAuth(ctx context.Context, databaseID, userID string, resetAuth *DatabaseResetUserAuthRequest) (*DatabaseUser, *Response, error) { path := fmt.Sprintf(databaseResetUserAuthPath, databaseID, userID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, resetAuth) if err != nil { return nil, nil, err } root := new(databaseUserRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.User, resp, nil } // DeleteUser will delete an existing database user func (svc *DatabasesServiceOp) DeleteUser(ctx context.Context, databaseID, userID string) (*Response, error) { path := fmt.Sprintf(databaseUserPath, databaseID, userID) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // ListDBs returns all databases for a given database cluster func (svc *DatabasesServiceOp) ListDBs(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseDB, *Response, error) { path := fmt.Sprintf(databaseDBsPath, databaseID) path, err := addOptions(path, opts) 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(databaseDBsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.DBs, resp, nil } // GetDB returns a single database by name func (svc *DatabasesServiceOp) GetDB(ctx context.Context, databaseID, name string) (*DatabaseDB, *Response, error) { path := fmt.Sprintf(databaseDBPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(databaseDBRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.DB, resp, nil } // CreateDB will create a new database func (svc *DatabasesServiceOp) CreateDB(ctx context.Context, databaseID string, createDB *DatabaseCreateDBRequest) (*DatabaseDB, *Response, error) { path := fmt.Sprintf(databaseDBsPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createDB) if err != nil { return nil, nil, err } root := new(databaseDBRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.DB, resp, nil } // DeleteDB will delete an existing database func (svc *DatabasesServiceOp) DeleteDB(ctx context.Context, databaseID, name string) (*Response, error) { path := fmt.Sprintf(databaseDBPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // ListPools returns all connection pools for a given database cluster func (svc *DatabasesServiceOp) ListPools(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabasePool, *Response, error) { path := fmt.Sprintf(databasePoolsPath, databaseID) path, err := addOptions(path, opts) 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(databasePoolsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Pools, resp, nil } // GetPool returns a single database connection pool by name func (svc *DatabasesServiceOp) GetPool(ctx context.Context, databaseID, name string) (*DatabasePool, *Response, error) { path := fmt.Sprintf(databasePoolPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(databasePoolRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Pool, resp, nil } // CreatePool will create a new database connection pool func (svc *DatabasesServiceOp) CreatePool(ctx context.Context, databaseID string, createPool *DatabaseCreatePoolRequest) (*DatabasePool, *Response, error) { path := fmt.Sprintf(databasePoolsPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createPool) if err != nil { return nil, nil, err } root := new(databasePoolRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Pool, resp, nil } // DeletePool will delete an existing database connection pool func (svc *DatabasesServiceOp) DeletePool(ctx context.Context, databaseID, name string) (*Response, error) { path := fmt.Sprintf(databasePoolPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // GetReplica returns a single database replica func (svc *DatabasesServiceOp) GetReplica(ctx context.Context, databaseID, name string) (*DatabaseReplica, *Response, error) { path := fmt.Sprintf(databaseReplicaPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(databaseReplicaRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Replica, resp, nil } // ListReplicas returns all read-only replicas for a given database cluster func (svc *DatabasesServiceOp) ListReplicas(ctx context.Context, databaseID string, opts *ListOptions) ([]DatabaseReplica, *Response, error) { path := fmt.Sprintf(databaseReplicasPath, databaseID) path, err := addOptions(path, opts) 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(databaseReplicasRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Replicas, resp, nil } // CreateReplica will create a new database connection pool func (svc *DatabasesServiceOp) CreateReplica(ctx context.Context, databaseID string, createReplica *DatabaseCreateReplicaRequest) (*DatabaseReplica, *Response, error) { path := fmt.Sprintf(databaseReplicasPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createReplica) if err != nil { return nil, nil, err } root := new(databaseReplicaRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Replica, resp, nil } // DeleteReplica will delete an existing database replica func (svc *DatabasesServiceOp) DeleteReplica(ctx context.Context, databaseID, name string) (*Response, error) { path := fmt.Sprintf(databaseReplicaPath, databaseID, name) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // GetEvictionPolicy loads the eviction policy for a given Redis cluster. func (svc *DatabasesServiceOp) GetEvictionPolicy(ctx context.Context, databaseID string) (string, *Response, error) { path := fmt.Sprintf(databaseEvictionPolicyPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return "", nil, err } root := new(evictionPolicyRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return "", resp, err } return root.EvictionPolicy, resp, nil } // SetEvictionPolicy updates the eviction policy for a given Redis cluster. // // The valid eviction policies are documented by the exported string constants // with the prefix `EvictionPolicy`. func (svc *DatabasesServiceOp) SetEvictionPolicy(ctx context.Context, databaseID, policy string) (*Response, error) { path := fmt.Sprintf(databaseEvictionPolicyPath, databaseID) root := &evictionPolicyRoot{EvictionPolicy: policy} req, err := svc.client.NewRequest(ctx, http.MethodPut, path, root) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // GetSQLMode loads the SQL Mode settings for a given MySQL cluster. func (svc *DatabasesServiceOp) GetSQLMode(ctx context.Context, databaseID string) (string, *Response, error) { path := fmt.Sprintf(databaseSQLModePath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return "", nil, err } root := &sqlModeRoot{} resp, err := svc.client.Do(ctx, req, root) if err != nil { return "", resp, err } return root.SQLMode, resp, nil } // SetSQLMode updates the SQL Mode settings for a given MySQL cluster. func (svc *DatabasesServiceOp) SetSQLMode(ctx context.Context, databaseID string, sqlModes ...string) (*Response, error) { path := fmt.Sprintf(databaseSQLModePath, databaseID) root := &sqlModeRoot{SQLMode: strings.Join(sqlModes, ",")} req, err := svc.client.NewRequest(ctx, http.MethodPut, path, root) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // GetFirewallRules loads the inbound sources for a given cluster. func (svc *DatabasesServiceOp) GetFirewallRules(ctx context.Context, databaseID string) ([]DatabaseFirewallRule, *Response, error) { path := fmt.Sprintf(databaseFirewallRulesPath, databaseID) root := new(databaseFirewallRuleRoot) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Rules, resp, nil } // UpdateFirewallRules sets the inbound sources for a given cluster. func (svc *DatabasesServiceOp) UpdateFirewallRules(ctx context.Context, databaseID string, firewallRulesReq *DatabaseUpdateFirewallRulesRequest) (*Response, error) { path := fmt.Sprintf(databaseFirewallRulesPath, databaseID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, firewallRulesReq) if err != nil { return nil, err } return svc.client.Do(ctx, req, nil) } godo-1.37.0/databases_test.go000066400000000000000000001013641366527000000160530ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/require" ) var privateNetworkUUID = "880b7f98-f062-404d-b33c-458d545696f6" var db = Database{ ID: "da4e0206-d019-41d7-b51f-deadbeefbb8f", Name: "dbtest", EngineSlug: "pg", VersionSlug: "11", Connection: &DatabaseConnection{ URI: "postgres://doadmin:zt91mum075ofzyww@dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", Database: "defaultdb", Host: "dbtest-do-user-3342561-0.db.ondigitalocean.com", Port: 25060, User: "doadmin", Password: "zt91mum075ofzyww", SSL: true, }, PrivateConnection: &DatabaseConnection{ URI: "postgres://doadmin:zt91mum075ofzyww@private-dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", Database: "defaultdb", Host: "dbtest-do-user-3342561-0.db.ondigitalocean.com", Port: 25060, User: "doadmin", Password: "zt91mum075ofzyww", SSL: true, }, Users: []DatabaseUser{ DatabaseUser{ Name: "doadmin", Role: "primary", Password: "zt91mum075ofzyww", }, }, DBNames: []string{ "defaultdb", }, NumNodes: 3, RegionSlug: "sfo2", Status: "online", CreatedAt: time.Date(2019, 2, 26, 6, 12, 39, 0, time.UTC), MaintenanceWindow: &DatabaseMaintenanceWindow{ Day: "monday", Hour: "13:51:14", Pending: false, Description: nil, }, SizeSlug: "db-s-2vcpu-4gb", PrivateNetworkUUID: "da4e0206-d019-41d7-b51f-deadbeefbb8f", Tags: []string{"production", "staging"}, } var dbJSON = ` { "id": "da4e0206-d019-41d7-b51f-deadbeefbb8f", "name": "dbtest", "engine": "pg", "version": "11", "connection": { "uri": "postgres://doadmin:zt91mum075ofzyww@dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", "database": "defaultdb", "host": "dbtest-do-user-3342561-0.db.ondigitalocean.com", "port": 25060, "user": "doadmin", "password": "zt91mum075ofzyww", "ssl": true }, "private_connection": { "uri": "postgres://doadmin:zt91mum075ofzyww@private-dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", "database": "defaultdb", "host": "dbtest-do-user-3342561-0.db.ondigitalocean.com", "port": 25060, "user": "doadmin", "password": "zt91mum075ofzyww", "ssl": true }, "users": [ { "name": "doadmin", "role": "primary", "password": "zt91mum075ofzyww" } ], "db_names": [ "defaultdb" ], "num_nodes": 3, "region": "sfo2", "status": "online", "created_at": "2019-02-26T06:12:39Z", "maintenance_window": { "day": "monday", "hour": "13:51:14", "pending": false, "description": null }, "size": "db-s-2vcpu-4gb", "private_network_uuid": "da4e0206-d019-41d7-b51f-deadbeefbb8f", "tags": ["production", "staging"] } ` var dbsJSON = fmt.Sprintf(` { "databases": [ %s ] } `, dbJSON) func TestDatabases_List(t *testing.T) { setup() defer teardown() dbSvc := client.Databases want := []Database{db} mux.HandleFunc("/v2/databases", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, dbsJSON) }) got, _, err := dbSvc.List(ctx, nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_Get(t *testing.T) { setup() defer teardown() dbID := "da4e0206-d019-41d7-b51f-deadbeefbb8f" body := fmt.Sprintf(` { "database": %s } `, dbJSON) path := fmt.Sprintf("/v2/databases/%s", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.Get(ctx, dbID) require.NoError(t, err) require.Equal(t, &db, got) } func TestDatabases_Create(t *testing.T) { setup() defer teardown() want := &Database{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "backend-test", EngineSlug: "pg", VersionSlug: "10", Connection: &DatabaseConnection{ URI: "postgres://doadmin:zt91mum075ofzyww@dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", Database: "defaultdb", Host: "dbtest-do-user-3342561-0.db.ondigitalocean.com", Port: 25060, User: "doadmin", Password: "zt91mum075ofzyww", SSL: true, }, PrivateConnection: &DatabaseConnection{ URI: "postgres://doadmin:zt91mum075ofzyww@private-dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", Database: "defaultdb", Host: "dbtest-do-user-3342561-0.db.ondigitalocean.com", Port: 25060, User: "doadmin", Password: "zt91mum075ofzyww", SSL: true, }, Users: nil, DBNames: nil, NumNodes: 2, RegionSlug: "nyc3", Status: "creating", CreatedAt: time.Date(2019, 2, 26, 6, 12, 39, 0, time.UTC), MaintenanceWindow: nil, SizeSlug: "db-s-2vcpu-4gb", Tags: []string{"production", "staging"}, } createRequest := &DatabaseCreateRequest{ Name: "backend-test", EngineSlug: "pg", Version: "10", Region: "nyc3", SizeSlug: "db-s-2vcpu-4gb", NumNodes: 2, Tags: []string{"production", "staging"}, } body := ` { "database": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "backend-test", "engine": "pg", "version": "10", "connection": { "uri": "postgres://doadmin:zt91mum075ofzyww@dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", "database": "defaultdb", "host": "dbtest-do-user-3342561-0.db.ondigitalocean.com", "port": 25060, "user": "doadmin", "password": "zt91mum075ofzyww", "ssl": true }, "private_connection": { "uri": "postgres://doadmin:zt91mum075ofzyww@private-dbtest-do-user-3342561-0.db.ondigitalocean.com:25060/defaultdb?sslmode=require", "database": "defaultdb", "host": "dbtest-do-user-3342561-0.db.ondigitalocean.com", "port": 25060, "user": "doadmin", "password": "zt91mum075ofzyww", "ssl": true }, "users": null, "db_names": null, "num_nodes": 2, "region": "nyc3", "status": "creating", "created_at": "2019-02-26T06:12:39Z", "maintenance_window": null, "size": "db-s-2vcpu-4gb", "tags": ["production", "staging"] } }` mux.HandleFunc("/v2/databases", func(w http.ResponseWriter, r *http.Request) { v := new(DatabaseCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, body) }) got, _, err := client.Databases.Create(ctx, createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_Delete(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Databases.Delete(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) } func TestDatabases_Resize(t *testing.T) { setup() defer teardown() resizeRequest := &DatabaseResizeRequest{ SizeSlug: "db-s-16vcpu-64gb", NumNodes: 3, } dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/resize", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.Resize(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", resizeRequest) require.NoError(t, err) } func TestDatabases_Migrate(t *testing.T) { setup() defer teardown() migrateRequest := &DatabaseMigrateRequest{ Region: "lon1", } dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/migrate", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.Migrate(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", migrateRequest) require.NoError(t, err) } func TestDatabases_Migrate_WithPrivateNet(t *testing.T) { setup() defer teardown() migrateRequest := &DatabaseMigrateRequest{ Region: "lon1", PrivateNetworkUUID: privateNetworkUUID, } dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/migrate", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.Migrate(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", migrateRequest) require.NoError(t, err) } func TestDatabases_UpdateMaintenance(t *testing.T) { setup() defer teardown() maintenanceRequest := &DatabaseUpdateMaintenanceRequest{ Day: "thursday", Hour: "16:00", } dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/maintenance", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.UpdateMaintenance(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", maintenanceRequest) require.NoError(t, err) } func TestDatabases_ListBackups(t *testing.T) { setup() defer teardown() want := []DatabaseBackup{ DatabaseBackup{ CreatedAt: time.Date(2019, 1, 11, 18, 42, 27, 0, time.UTC), SizeGigabytes: 0.03357696, }, DatabaseBackup{ CreatedAt: time.Date(2019, 1, 12, 18, 42, 29, 0, time.UTC), SizeGigabytes: 0.03364864, }, } body := ` { "backups": [ { "created_at": "2019-01-11T18:42:27Z", "size_gigabytes": 0.03357696 }, { "created_at": "2019-01-12T18:42:29Z", "size_gigabytes": 0.03364864 } ] } ` dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/backups", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.ListBackups(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_GetUser(t *testing.T) { setup() defer teardown() want := &DatabaseUser{ Name: "name", Role: "foo", Password: "pass", } body := ` { "user": { "name": "name", "role": "foo", "password": "pass" } } ` dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/users/name", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetUser(ctx, dbID, "name") require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_ListUsers(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := []DatabaseUser{ { Name: "name", Role: "foo", Password: "pass", }, { Name: "bar", Role: "foo", Password: "pass", }, } body := ` { "users": [{ "name": "name", "role": "foo", "password": "pass" }, { "name": "bar", "role": "foo", "password": "pass" }] } ` path := fmt.Sprintf("/v2/databases/%s/users", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.ListUsers(ctx, dbID, nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_CreateUser(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := &DatabaseUser{ Name: "name", Role: "foo", Password: "pass", } body := ` { "user": { "name": "name", "role": "foo", "password": "pass" } } ` path := fmt.Sprintf("/v2/databases/%s/users", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint(w, body) }) got, _, err := client.Databases.CreateUser(ctx, dbID, &DatabaseCreateUserRequest{Name: "user"}) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_DeleteUser(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/users/user", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Databases.DeleteUser(ctx, dbID, "user") require.NoError(t, err) } func TestDatabases_ResetUserAuth(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/users/user/reset_auth", dbID) body := ` { "user": { "name": "name", "role": "foo", "password": "pass", "mysql_settings": { "auth_plugin": "caching_sha2_password" } } } ` mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint(w, body) }) want := &DatabaseUser{ Name: "name", Role: "foo", Password: "pass", MySQLSettings: &DatabaseMySQLUserSettings{AuthPlugin: SQLAuthPluginCachingSHA2}, } got, _, err := client.Databases.ResetUserAuth(ctx, dbID, "user", &DatabaseResetUserAuthRequest{ MySQLSettings: &DatabaseMySQLUserSettings{ AuthPlugin: SQLAuthPluginCachingSHA2, }}) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_ListDBs(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := []DatabaseDB{ {Name: "foo"}, {Name: "bar"}, } body := ` { "dbs": [{ "name": "foo" }, { "name": "bar" }] } ` path := fmt.Sprintf("/v2/databases/%s/dbs", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.ListDBs(ctx, dbID, nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_CreateDB(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := &DatabaseDB{ Name: "foo", } body := ` { "db": { "name": "foo" } } ` path := fmt.Sprintf("/v2/databases/%s/dbs", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint(w, body) }) got, _, err := client.Databases.CreateDB(ctx, dbID, &DatabaseCreateDBRequest{Name: "foo"}) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_GetDB(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := &DatabaseDB{ Name: "foo", } body := ` { "db": { "name": "foo" } } ` path := fmt.Sprintf("/v2/databases/%s/dbs/foo", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetDB(ctx, dbID, "foo") require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_DeleteDB(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" body := ` { "db": { "name": "foo" } } ` path := fmt.Sprintf("/v2/databases/%s/dbs/foo", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) fmt.Fprint(w, body) }) _, err := client.Databases.DeleteDB(ctx, dbID, "foo") require.NoError(t, err) } func TestDatabases_ListPools(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := []DatabasePool{ { Name: "pool", User: "user", Size: 10, Mode: "transaction", Database: "db", Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, }, } body := ` { "pools": [{ "name": "pool", "user": "user", "size": 10, "mode": "transaction", "db": "db", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true } }] } ` path := fmt.Sprintf("/v2/databases/%s/pools", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.ListPools(ctx, dbID, nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_CreatePool(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := &DatabasePool{ Name: "pool", User: "user", Size: 10, Mode: "transaction", Database: "db", Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, } body := ` { "pool": { "name": "pool", "user": "user", "size": 10, "mode": "transaction", "db": "db", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true } } } ` path := fmt.Sprintf("/v2/databases/%s/pools", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint(w, body) }) got, _, err := client.Databases.CreatePool(ctx, dbID, &DatabaseCreatePoolRequest{ Name: "pool", Database: "db", Size: 10, User: "foo", Mode: "transaction", }) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_GetPool(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := &DatabasePool{ Name: "pool", User: "user", Size: 10, Mode: "transaction", Database: "db", Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, } body := ` { "pool": { "name": "pool", "user": "user", "size": 10, "mode": "transaction", "db": "db", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true } } } ` path := fmt.Sprintf("/v2/databases/%s/pools/pool", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetPool(ctx, dbID, "pool") require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_DeletePool(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/pools/pool", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Databases.DeletePool(ctx, dbID, "pool") require.NoError(t, err) } func TestDatabases_GetReplica(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" createdAt := time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC) want := &DatabaseReplica{ Name: "pool", Region: "nyc1", Status: "online", CreatedAt: createdAt, Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateNetworkUUID: "deadbeef-dead-4aa5-beef-deadbeef347d", Tags: []string{"production", "staging"}, } body := ` { "replica": { "name": "pool", "region": "nyc1", "status": "online", "created_at": "` + createdAt.Format(time.RFC3339) + `", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_network_uuid": "deadbeef-dead-4aa5-beef-deadbeef347d", "tags": ["production", "staging"] } } ` path := fmt.Sprintf("/v2/databases/%s/replicas/replica", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetReplica(ctx, dbID, "replica") require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_ListReplicas(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" createdAt := time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC) want := []DatabaseReplica{ { Name: "pool", Region: "nyc1", Status: "online", CreatedAt: createdAt, Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateNetworkUUID: "deadbeef-dead-4aa5-beef-deadbeef347d", Tags: []string{"production", "staging"}, }, } body := ` { "replicas": [{ "name": "pool", "region": "nyc1", "status": "online", "created_at": "` + createdAt.Format(time.RFC3339) + `", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_network_uuid": "deadbeef-dead-4aa5-beef-deadbeef347d", "tags": ["production", "staging"] }] } ` path := fmt.Sprintf("/v2/databases/%s/replicas", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.ListReplicas(ctx, dbID, nil) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_CreateReplica(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" createdAt := time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC) want := &DatabaseReplica{ Name: "pool", Region: "nyc1", Status: "online", CreatedAt: createdAt, Connection: &DatabaseConnection{ URI: "postgresql://user:pass@host.com/db", Host: "host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateConnection: &DatabaseConnection{ URI: "postgresql://user:pass@private-host.com/db", Host: "private-host.com", Port: 1234, User: "user", Password: "pass", SSL: true, Database: "db", }, PrivateNetworkUUID: "deadbeef-dead-4aa5-beef-deadbeef347d", Tags: []string{"production", "staging"}, } body := ` { "replica": { "name": "pool", "region": "nyc1", "status": "online", "created_at": "` + createdAt.Format(time.RFC3339) + `", "connection": { "uri": "postgresql://user:pass@host.com/db", "host": "host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_connection": { "uri": "postgresql://user:pass@private-host.com/db", "host": "private-host.com", "port": 1234, "user": "user", "password": "pass", "database": "db", "ssl": true }, "private_network_uuid": "deadbeef-dead-4aa5-beef-deadbeef347d", "tags": ["production", "staging"] } } ` path := fmt.Sprintf("/v2/databases/%s/replicas", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) fmt.Fprint(w, body) }) got, _, err := client.Databases.CreateReplica(ctx, dbID, &DatabaseCreateReplicaRequest{ Name: "replica", Region: "nyc1", Size: "db-s-2vcpu-4gb", PrivateNetworkUUID: privateNetworkUUID, Tags: []string{"production", "staging"}, }) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_DeleteReplica(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/replicas/replica", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Databases.DeleteReplica(ctx, dbID, "replica") require.NoError(t, err) } func TestDatabases_SetEvictionPolicy(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/eviction_policy", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.SetEvictionPolicy(ctx, dbID, "allkeys_lru") require.NoError(t, err) } func TestDatabases_GetEvictionPolicy(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := "allkeys_lru" body := `{ "eviction_policy": "allkeys_lru" }` path := fmt.Sprintf("/v2/databases/%s/eviction_policy", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetEvictionPolicy(ctx, dbID) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_SetSQLMode(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/sql_mode", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) w.Write([]byte(`{ "sql_mode": "ONLY_FULL_GROUP_BY" }`)) }) _, err := client.Databases.SetSQLMode(ctx, dbID, "ONLY_FULL_GROUP_BY") require.NoError(t, err) } func TestDatabases_SetSQLMode_Multiple(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/sql_mode", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) w.Write([]byte(`{ "sql_mode": "ANSI, ANSI_QUOTES" }`)) }) _, err := client.Databases.SetSQLMode(ctx, dbID, SQLModeANSI, SQLModeANSIQuotes) require.NoError(t, err) } func TestDatabases_GetSQLMode(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" want := "ONLY_FULL_GROUP_BY" body := `{ "sql_mode": "ONLY_FULL_GROUP_BY" }` path := fmt.Sprintf("/v2/databases/%s/sql_mode", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetSQLMode(ctx, dbID) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_GetFirewallRules(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/firewall", dbID) want := []DatabaseFirewallRule{ { Type: "ip_addr", Value: "192.168.1.1", UUID: "deadbeef-dead-4aa5-beef-deadbeef347d", ClusterUUID: "deadbeef-dead-4aa5-beef-deadbeef347d", }, } body := ` {"rules": [{ "type": "ip_addr", "value": "192.168.1.1", "uuid": "deadbeef-dead-4aa5-beef-deadbeef347d", "cluster_uuid": "deadbeef-dead-4aa5-beef-deadbeef347d" }]} ` mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, body) }) got, _, err := client.Databases.GetFirewallRules(ctx, dbID) require.NoError(t, err) require.Equal(t, want, got) } func TestDatabases_UpdateFirewallRules(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/firewall", dbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPut) }) _, err := client.Databases.UpdateFirewallRules(ctx, dbID, &DatabaseUpdateFirewallRulesRequest{ Rules: []*DatabaseFirewallRule{ { Type: "ip_addr", Value: "192.168.1.1", UUID: "deadbeef-dead-4aa5-beef-deadbeef347d", }, }, }) require.NoError(t, err) } func TestDatabases_CreateDatabaseUserWithMySQLSettings(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/users", dbID) responseJSON := []byte(fmt.Sprintf(`{ "user": { "name": "foo", "mysql_settings": { "auth_plugin": "%s" } } }`, SQLAuthPluginNative)) expectedUser := &DatabaseUser{ Name: "foo", MySQLSettings: &DatabaseMySQLUserSettings{ AuthPlugin: SQLAuthPluginNative, }, } mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) w.WriteHeader(http.StatusOK) w.Write(responseJSON) }) user, _, err := client.Databases.CreateUser(ctx, dbID, &DatabaseCreateUserRequest{ Name: expectedUser.Name, MySQLSettings: expectedUser.MySQLSettings, }) require.NoError(t, err) require.Equal(t, expectedUser, user) } func TestDatabases_ListDatabaseUsersWithMySQLSettings(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" path := fmt.Sprintf("/v2/databases/%s/users", dbID) responseJSON := []byte(fmt.Sprintf(`{ "users": [ { "name": "foo", "mysql_settings": { "auth_plugin": "%s" } } ] }`, SQLAuthPluginNative)) expectedUsers := []DatabaseUser{ { Name: "foo", MySQLSettings: &DatabaseMySQLUserSettings{ AuthPlugin: SQLAuthPluginNative, }, }, } mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) w.WriteHeader(http.StatusOK) w.Write(responseJSON) }) users, _, err := client.Databases.ListUsers(ctx, dbID, &ListOptions{}) require.NoError(t, err) require.Equal(t, expectedUsers, users) } func TestDatabases_GetDatabaseUserWithMySQLSettings(t *testing.T) { setup() defer teardown() dbID := "deadbeef-dead-4aa5-beef-deadbeef347d" userID := "d290a0a0-27da-42bd-a4b2-bcecf43b8832" path := fmt.Sprintf("/v2/databases/%s/users/%s", dbID, userID) responseJSON := []byte(fmt.Sprintf(`{ "user": { "name": "foo", "mysql_settings": { "auth_plugin": "%s" } } }`, SQLAuthPluginNative)) expectedUser := &DatabaseUser{ Name: "foo", MySQLSettings: &DatabaseMySQLUserSettings{ AuthPlugin: SQLAuthPluginNative, }, } mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) w.WriteHeader(http.StatusOK) w.Write(responseJSON) }) user, _, err := client.Databases.GetUser(ctx, dbID, userID) require.NoError(t, err) require.Equal(t, expectedUser, user) } godo-1.37.0/doc.go000066400000000000000000000001061366527000000136220ustar00rootroot00000000000000// Package godo is the DigtalOcean API v2 client for Go. package godo godo-1.37.0/domains.go000066400000000000000000000216541366527000000145220ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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"` Meta *Meta `json:"meta"` } // DomainCreateRequest respresents a request to create a domain. type DomainCreateRequest struct { Name string `json:"name"` IPAddress string `json:"ip_address,omitempty"` } // 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"` Port int `json:"port,omitempty"` TTL int `json:"ttl,omitempty"` Weight int `json:"weight"` Flags int `json:"flags"` Tag string `json:"tag,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"` Port int `json:"port,omitempty"` TTL int `json:"ttl,omitempty"` Weight int `json:"weight"` Flags int `json:"flags"` Tag string `json:"tag,omitempty"` } func (d Domain) String() string { return Stringify(d) } func (d Domain) URN() string { return ToURN("Domain", d.Name) } // 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 } if m := root.Meta; m != nil { resp.Meta = m } 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, http.MethodPut, path, editRequest) if err != nil { return nil, nil, err } root := new(domainRecordRoot) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.DomainRecord, 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 } godo-1.37.0/domains_test.go000066400000000000000000000222311366527000000155510ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestDomains_ListDomains(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "domains": [ { "name":"foo.com" }, { "name":"bar.com" } ], "meta": { "total": 2 } }`) }) domains, resp, err := client.Domains.List(ctx, nil) if err != nil { t.Errorf("Domains.List returned error: %v", err) } expectedDomains := []Domain{{Name: "foo.com"}, {Name: "bar.com"}} if !reflect.DeepEqual(domains, expectedDomains) { t.Errorf("Domains.List returned domains %+v, expected %+v", domains, expectedDomains) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Domains.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestDomains_ListDomainsMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"domains": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/domains/?page=2"}}}`) }) _, resp, err := client.Domains.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestDomains_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "domains": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/domains/?page=3", "prev":"http://example.com/v2/domains/?page=1", "last":"http://example.com/v2/domains/?page=3", "first":"http://example.com/v2/domains/?page=1" } }, "meta":{ "total":2 } }` mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Domains.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestDomains_GetDomain(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"domain":{"name":"example.com"}}`) }) domains, _, err := client.Domains.Get(ctx, "example.com") if err != nil { t.Errorf("domain.Get returned error: %v", err) } expected := &Domain{Name: "example.com"} if !reflect.DeepEqual(domains, expected) { t.Errorf("domains.Get returned %+v, expected %+v", domains, expected) } } func TestDomains_Create(t *testing.T) { setup() defer teardown() createRequest := &DomainCreateRequest{ Name: "example.com", IPAddress: "127.0.0.1", } mux.HandleFunc("/v2/domains", func(w http.ResponseWriter, r *http.Request) { v := new(DomainCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, `{"domain":{"name":"example.com"}}`) }) domain, _, err := client.Domains.Create(ctx, createRequest) if err != nil { t.Errorf("Domains.Create returned error: %v", err) } expected := &Domain{Name: "example.com"} if !reflect.DeepEqual(domain, expected) { t.Errorf("Domains.Create returned %+v, expected %+v", domain, expected) } } func TestDomains_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Domains.Delete(ctx, "example.com") if err != nil { t.Errorf("Domains.Delete returned error: %v", err) } } func TestDomains_AllRecordsForDomainName(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`) }) records, _, err := client.Domains.Records(ctx, "example.com", nil) if err != nil { t.Errorf("Domains.List returned error: %v", err) } expected := []DomainRecord{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(records, expected) { t.Errorf("Domains.List returned %+v, expected %+v", records, expected) } } func TestDomains_AllRecordsForDomainName_PerPage(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) { perPage := r.URL.Query().Get("per_page") if perPage != "2" { t.Fatalf("expected '2', got '%s'", perPage) } fmt.Fprint(w, `{"domain_records":[{"id":1},{"id":2}]}`) }) dro := &ListOptions{PerPage: 2} records, _, err := client.Domains.Records(ctx, "example.com", dro) if err != nil { t.Errorf("Domains.List returned error: %v", err) } expected := []DomainRecord{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(records, expected) { t.Errorf("Domains.List returned %+v, expected %+v", records, expected) } } func TestDomains_GetRecordforDomainName(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"domain_record":{"id":1}}`) }) record, _, err := client.Domains.Record(ctx, "example.com", 1) if err != nil { t.Errorf("Domains.GetRecord returned error: %v", err) } expected := &DomainRecord{ID: 1} if !reflect.DeepEqual(record, expected) { t.Errorf("Domains.GetRecord returned %+v, expected %+v", record, expected) } } func TestDomains_DeleteRecordForDomainName(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Domains.DeleteRecord(ctx, "example.com", 1) if err != nil { t.Errorf("Domains.RecordDelete returned error: %v", err) } } func TestDomains_CreateRecordForDomainName(t *testing.T) { setup() defer teardown() createRequest := &DomainRecordEditRequest{ Type: "CNAME", Name: "example", Data: "@", Priority: 10, Port: 10, TTL: 1800, Weight: 10, Flags: 1, Tag: "test", } mux.HandleFunc("/v2/domains/example.com/records", func(w http.ResponseWriter, r *http.Request) { v := new(DomainRecordEditRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprintf(w, `{"domain_record": {"id":1}}`) }) record, _, err := client.Domains.CreateRecord(ctx, "example.com", createRequest) if err != nil { t.Errorf("Domains.CreateRecord returned error: %v", err) } expected := &DomainRecord{ID: 1} if !reflect.DeepEqual(record, expected) { t.Errorf("Domains.CreateRecord returned %+v, expected %+v", record, expected) } } func TestDomains_EditRecordForDomainName(t *testing.T) { setup() defer teardown() editRequest := &DomainRecordEditRequest{ Type: "CNAME", Name: "example", Data: "@", Priority: 10, Port: 10, TTL: 1800, Weight: 10, Flags: 1, Tag: "test", } mux.HandleFunc("/v2/domains/example.com/records/1", func(w http.ResponseWriter, r *http.Request) { v := new(DomainRecordEditRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPut) if !reflect.DeepEqual(v, editRequest) { t.Errorf("Request body = %+v, expected %+v", v, editRequest) } fmt.Fprintf(w, `{"domain_record": {"id":1, "type": "CNAME", "name": "example"}}`) }) record, _, err := client.Domains.EditRecord(ctx, "example.com", 1, editRequest) if err != nil { t.Errorf("Domains.EditRecord returned error: %v", err) } expected := &DomainRecord{ID: 1, Type: "CNAME", Name: "example"} if !reflect.DeepEqual(record, expected) { t.Errorf("Domains.EditRecord returned %+v, expected %+v", record, expected) } } func TestDomainRecord_String(t *testing.T) { record := &DomainRecord{ ID: 1, Type: "CNAME", Name: "example", Data: "@", Priority: 10, Port: 10, TTL: 1800, Weight: 10, Flags: 1, Tag: "test", } stringified := record.String() expected := `godo.DomainRecord{ID:1, Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10, Flags:1, Tag:"test"}` if expected != stringified { t.Errorf("DomainRecord.String returned %+v, expected %+v", stringified, expected) } } func TestDomainRecordEditRequest_String(t *testing.T) { record := &DomainRecordEditRequest{ Type: "CNAME", Name: "example", Data: "@", Priority: 10, Port: 10, TTL: 1800, Weight: 10, Flags: 1, Tag: "test", } stringified := record.String() expected := `godo.DomainRecordEditRequest{Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10, Flags:1, Tag:"test"}` if expected != stringified { t.Errorf("DomainRecordEditRequest.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/droplet_actions.go000066400000000000000000000272231366527000000162570ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" "net/url" ) // 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) 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) } 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) } godo-1.37.0/droplet_actions_test.go000066400000000000000000000623741366527000000173240ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestDropletActions_Shutdown(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "shutdown", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Shutdown(ctx, 1) if err != nil { t.Errorf("DropletActions.Shutdown returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Shutdown returned %+v, expected %+v", action, expected) } } func TestDropletActions_ShutdownByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "shutdown", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.ShutdownByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.ShutdownByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.ShutdownByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.ShutdownByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerOff(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_off", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.PowerOff(ctx, 1) if err != nil { t.Errorf("DropletActions.PowerOff returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Poweroff returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerOffByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_off", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.PowerOffByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.PowerOffByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.PowerOffByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PoweroffByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerOn(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_on", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.PowerOn(ctx, 1) if err != nil { t.Errorf("DropletActions.PowerOn returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PowerOn returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerOnByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_on", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.PowerOnByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.PowerOnByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.PowerOnByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PowerOnByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_Reboot(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "reboot", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Reboot(ctx, 1) if err != nil { t.Errorf("DropletActions.Reboot returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Reboot returned %+v, expected %+v", action, expected) } } func TestDropletAction_Restore(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "restore", "image": float64(1), } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Restore(ctx, 1, 1) if err != nil { t.Errorf("DropletActions.Restore returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Restore returned %+v, expected %+v", action, expected) } } func TestDropletAction_Resize(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "resize", "size": "1024mb", "disk": true, } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Resize(ctx, 1, "1024mb", true) if err != nil { t.Errorf("DropletActions.Resize returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Resize returned %+v, expected %+v", action, expected) } } func TestDropletAction_Rename(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "rename", "name": "Droplet-Name", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Rename(ctx, 1, "Droplet-Name") if err != nil { t.Errorf("DropletActions.Rename returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Rename returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerCycle(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_cycle", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.PowerCycle(ctx, 1) if err != nil { t.Errorf("DropletActions.PowerCycle returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PowerCycle returned %+v, expected %+v", action, expected) } } func TestDropletAction_PowerCycleByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "power_cycle", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.PowerCycleByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.PowerCycleByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.PowerCycleByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PowerCycleByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_Snapshot(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "snapshot", "name": "Image-Name", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Snapshot(ctx, 1, "Image-Name") if err != nil { t.Errorf("DropletActions.Snapshot returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Snapshot returned %+v, expected %+v", action, expected) } } func TestDropletAction_SnapshotByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "snapshot", "name": "Image-Name", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.SnapshotByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.SnapshotByTag(ctx, "testing-1", "Image-Name") if err != nil { t.Errorf("DropletActions.SnapshotByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.SnapshotByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnableBackups(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_backups", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.EnableBackups(ctx, 1) if err != nil { t.Errorf("DropletActions.EnableBackups returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnableBackups returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnableBackupsByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_backups", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.EnableBackupByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.EnableBackupsByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.EnableBackupsByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnableBackupsByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_DisableBackups(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "disable_backups", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.DisableBackups(ctx, 1) if err != nil { t.Errorf("DropletActions.DisableBackups returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.DisableBackups returned %+v, expected %+v", action, expected) } } func TestDropletAction_DisableBackupsByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "disable_backups", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.DisableBackupsByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.DisableBackupsByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.DisableBackupsByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.DisableBackupsByTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_PasswordReset(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "password_reset", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.PasswordReset(ctx, 1) if err != nil { t.Errorf("DropletActions.PasswordReset returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.PasswordReset returned %+v, expected %+v", action, expected) } } func TestDropletAction_RebuildByImageID(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "rebuild", "image": float64(2), } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = \n%#v, expected \n%#v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.RebuildByImageID(ctx, 1, 2) if err != nil { t.Errorf("DropletActions.RebuildByImageID returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.RebuildByImageID returned %+v, expected %+v", action, expected) } } func TestDropletAction_RebuildByImageSlug(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "rebuild", "image": "Image-Name", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.RebuildByImageSlug(ctx, 1, "Image-Name") if err != nil { t.Errorf("DropletActions.RebuildByImageSlug returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.RebuildByImageSlug returned %+v, expected %+v", action, expected) } } func TestDropletAction_ChangeKernel(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "change_kernel", "kernel": float64(2), } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.ChangeKernel(ctx, 1, 2) if err != nil { t.Errorf("DropletActions.ChangeKernel returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.ChangeKernel returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnableIPv6(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_ipv6", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.EnableIPv6(ctx, 1) if err != nil { t.Errorf("DropletActions.EnableIPv6 returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnableIPv6 returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnableIPv6ByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_ipv6", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.EnableIPv6ByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.EnableIPv6ByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.EnableIPv6ByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnableIPv6byTag returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnablePrivateNetworking(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_private_networking", } mux.HandleFunc("/v2/droplets/1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.EnablePrivateNetworking(ctx, 1) if err != nil { t.Errorf("DropletActions.EnablePrivateNetworking returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnablePrivateNetworking returned %+v, expected %+v", action, expected) } } func TestDropletAction_EnablePrivateNetworkingByTag(t *testing.T) { setup() defer teardown() request := &ActionRequest{ "type": "enable_private_networking", } mux.HandleFunc("/v2/droplets/actions", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("DropletActions.EnablePrivateNetworkingByTag did not request with a tag parameter") } v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, request) { t.Errorf("Request body = %+v, expected %+v", v, request) } fmt.Fprint(w, `{"actions": [{"status":"in-progress"},{"status":"in-progress"}]}`) }) action, _, err := client.DropletActions.EnablePrivateNetworkingByTag(ctx, "testing-1") if err != nil { t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned error: %v", err) } expected := []Action{{Status: "in-progress"}, {Status: "in-progress"}} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.EnablePrivateNetworkingByTag returned %+v, expected %+v", action, expected) } } func TestDropletActions_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/123/actions/456", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.DropletActions.Get(ctx, 123, 456) if err != nil { t.Errorf("DropletActions.Get returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("DropletActions.Get returned %+v, expected %+v", action, expected) } } godo-1.37.0/droplets.go000066400000000000000000000400311366527000000147120ustar00rootroot00000000000000package godo import ( "context" "encoding/json" "errors" "fmt" "net/http" ) 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"` VPCUUID string `json:"vpc_uuid,omitempty"` } // 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) } func (d Droplet) URN() string { return ToURN("Droplet", d.ID) } // 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"` Meta *Meta `json:"meta"` } type kernelsRoot struct { Kernels []Kernel `json:"kernels,omitempty"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } type dropletSnapshotsRoot struct { Snapshots []Image `json:"snapshots,omitempty"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } type backupsRoot struct { Backups []Image `json:"backups,omitempty"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // 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"` VPCUUID string `json:"vpc_uuid,omitempty"` } // 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"` VPCUUID string `json:"vpc_uuid,omitempty"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } godo-1.37.0/droplets_test.go000066400000000000000000000347171366527000000157670ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestDroplets_ListDroplets(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "droplets": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) droplets, resp, err := client.Droplets.List(ctx, nil) if err != nil { t.Errorf("Droplets.List returned error: %v", err) } expectedDroplets := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(droplets, expectedDroplets) { t.Errorf("Droplets.List\nDroplets: got=%#v\nwant=%#v", droplets, expectedDroplets) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.List\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestDroplets_ListDropletsByTag(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("Droplets.ListByTag did not request with a tag parameter") } testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "droplets": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) droplets, resp, err := client.Droplets.ListByTag(ctx, "testing-1", nil) if err != nil { t.Errorf("Droplets.ListByTag returned error: %v", err) } expectedDroplets := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(droplets, expectedDroplets) { t.Errorf("Droplets.ListByTag returned droplets %+v, expected %+v", droplets, expectedDroplets) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.ListByTag returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestDroplets_ListDropletsMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) dr := dropletsRoot{ Droplets: []Droplet{ {ID: 1}, {ID: 2}, }, Links: &Links{ Pages: &Pages{Next: "http://example.com/v2/droplets/?page=2"}, }, } b, err := json.Marshal(dr) if err != nil { t.Fatal(err) } fmt.Fprint(w, string(b)) }) _, resp, err := client.Droplets.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestDroplets_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "droplets": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/droplets/?page=3", "prev":"http://example.com/v2/droplets/?page=1", "last":"http://example.com/v2/droplets/?page=3", "first":"http://example.com/v2/droplets/?page=1" } } }` mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Droplets.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestDroplets_GetDroplet(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"droplet":{"id":12345}}`) }) droplets, _, err := client.Droplets.Get(ctx, 12345) if err != nil { t.Errorf("Droplet.Get returned error: %v", err) } expected := &Droplet{ID: 12345} if !reflect.DeepEqual(droplets, expected) { t.Errorf("Droplets.Get\n got=%#v\nwant=%#v", droplets, expected) } } func TestDroplets_Create(t *testing.T) { setup() defer teardown() createRequest := &DropletCreateRequest{ Name: "name", Region: "region", Size: "size", Image: DropletCreateImage{ ID: 1, }, Volumes: []DropletCreateVolume{ {Name: "hello-im-a-volume"}, {ID: "hello-im-another-volume"}, {Name: "hello-im-still-a-volume", ID: "should be ignored due to Name"}, }, Tags: []string{"one", "two"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "name": "name", "region": "region", "size": "size", "image": float64(1), "ssh_keys": nil, "backups": false, "ipv6": false, "private_networking": false, "monitoring": false, "volumes": []interface{}{ map[string]interface{}{"name": "hello-im-a-volume"}, map[string]interface{}{"id": "hello-im-another-volume"}, map[string]interface{}{"name": "hello-im-still-a-volume"}, }, "tags": []interface{}{"one", "two"}, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", } jsonBlob := ` { "droplet": { "id": 1, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6" }, "links": { "actions": [ { "id": 1, "href": "http://example.com", "rel": "create" } ] } } ` var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) } fmt.Fprintf(w, jsonBlob) }) droplet, resp, err := client.Droplets.Create(ctx, createRequest) if err != nil { t.Errorf("Droplets.Create returned error: %v", err) } if id := droplet.ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } vpcid := "880b7f98-f062-404d-b33c-458d545696f6" if id := droplet.VPCUUID; id != vpcid { t.Errorf("expected VPC uuid '%s', received '%s'", vpcid, id) } if a := resp.Links.Actions[0]; a.ID != 1 { t.Errorf("expected action id '%d', received '%d'", 1, a.ID) } } func TestDroplets_CreateMultiple(t *testing.T) { setup() defer teardown() createRequest := &DropletMultiCreateRequest{ Names: []string{"name1", "name2"}, Region: "region", Size: "size", Image: DropletCreateImage{ ID: 1, }, Tags: []string{"one", "two"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "names": []interface{}{"name1", "name2"}, "region": "region", "size": "size", "image": float64(1), "ssh_keys": nil, "backups": false, "ipv6": false, "private_networking": false, "monitoring": false, "tags": []interface{}{"one", "two"}, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", } jsonBlob := ` { "droplets": [ { "id": 1, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6" }, { "id": 2, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6" } ], "links": { "actions": [ { "id": 1, "href": "http://example.com", "rel": "multiple_create" } ] } } ` var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body = %#v, expected %#v", v, expected) } fmt.Fprintf(w, jsonBlob) }) droplets, resp, err := client.Droplets.CreateMultiple(ctx, createRequest) if err != nil { t.Errorf("Droplets.CreateMultiple returned error: %v", err) } if id := droplets[0].ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } if id := droplets[1].ID; id != 2 { t.Errorf("expected id '%d', received '%d'", 2, id) } vpcid := "880b7f98-f062-404d-b33c-458d545696f6" if id := droplets[0].VPCUUID; id != vpcid { t.Errorf("expected VPC uuid '%s', received '%s'", vpcid, id) } if id := droplets[1].VPCUUID; id != vpcid { t.Errorf("expected VPC uuid '%s', received '%s'", vpcid, id) } if a := resp.Links.Actions[0]; a.ID != 1 { t.Errorf("expected action id '%d', received '%d'", 1, a.ID) } } func TestDroplets_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Droplets.Delete(ctx, 12345) if err != nil { t.Errorf("Droplet.Delete returned error: %v", err) } } func TestDroplets_DestroyByTag(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("tag_name") != "testing-1" { t.Errorf("Droplets.DeleteByTag did not request with a tag parameter") } testMethod(t, r, http.MethodDelete) }) _, err := client.Droplets.DeleteByTag(ctx, "testing-1") if err != nil { t.Errorf("Droplet.Delete returned error: %v", err) } } func TestDroplets_Kernels(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345/kernels", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "kernels": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) opt := &ListOptions{Page: 2} kernels, resp, err := client.Droplets.Kernels(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Kernels returned error: %v", err) } expectedKernels := []Kernel{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(kernels, expectedKernels) { t.Errorf("Droplets.Kernels\nKernels got=%#v\nwant=%#v", kernels, expectedKernels) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.Kernels\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestDroplets_Snapshots(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "snapshots": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) opt := &ListOptions{Page: 2} snapshots, resp, err := client.Droplets.Snapshots(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Snapshots returned error: %v", err) } expectedSnapshots := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(snapshots, expectedSnapshots) { t.Errorf("Droplets.Snapshots\nSnapshots got=%#v\nwant=%#v", snapshots, expectedSnapshots) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.Snapshots\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestDroplets_Backups(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345/backups", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "backups": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) opt := &ListOptions{Page: 2} backups, resp, err := client.Droplets.Backups(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Backups returned error: %v", err) } expectedBackups := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(backups, expectedBackups) { t.Errorf("Droplets.Backups\nBackups got=%#v\nwant=%#v", backups, expectedBackups) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.Backups\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestDroplets_Actions(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "actions": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) opt := &ListOptions{Page: 2} actions, resp, err := client.Droplets.Actions(ctx, 12345, opt) if err != nil { t.Errorf("Droplets.Actions returned error: %v", err) } expectedActions := []Action{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(actions, expectedActions) { t.Errorf("Droplets.Actions\nActions got=%#v\nwant=%#v", actions, expectedActions) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Droplets.Actions\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestDroplets_Neighbors(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/12345/neighbors", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`) }) neighbors, _, err := client.Droplets.Neighbors(ctx, 12345) if err != nil { t.Errorf("Droplets.Neighbors returned error: %v", err) } expected := []Droplet{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(neighbors, expected) { t.Errorf("Droplets.Neighbors\n got=%#v\nwant=%#v", neighbors, expected) } } func TestNetworkV4_String(t *testing.T) { network := &NetworkV4{ IPAddress: "192.168.1.2", Netmask: "255.255.255.0", Gateway: "192.168.1.1", } stringified := network.String() expected := `godo.NetworkV4{IPAddress:"192.168.1.2", Netmask:"255.255.255.0", Gateway:"192.168.1.1", Type:""}` if expected != stringified { t.Errorf("NetworkV4.String\n got=%#v\nwant=%#v", stringified, expected) } } func TestNetworkV6_String(t *testing.T) { network := &NetworkV6{ IPAddress: "2604:A880:0800:0010:0000:0000:02DD:4001", Netmask: 64, Gateway: "2604:A880:0800:0010:0000:0000:0000:0001", } stringified := network.String() expected := `godo.NetworkV6{IPAddress:"2604:A880:0800:0010:0000:0000:02DD:4001", Netmask:64, Gateway:"2604:A880:0800:0010:0000:0000:0000:0001", Type:""}` if expected != stringified { t.Errorf("NetworkV6.String\n got=%#v\nwant=%#v", stringified, expected) } } func TestDroplets_IPMethods(t *testing.T) { var d Droplet ipv6 := "1000:1000:1000:1000:0000:0000:004D:B001" d.Networks = &Networks{ V4: []NetworkV4{ {IPAddress: "192.168.0.1", Type: "public"}, {IPAddress: "10.0.0.1", Type: "private"}, }, V6: []NetworkV6{ {IPAddress: ipv6, Type: "public"}, }, } ip, err := d.PublicIPv4() if err != nil { t.Errorf("unknown error") } if got, expected := ip, "192.168.0.1"; got != expected { t.Errorf("Droplet.PublicIPv4 returned %s; expected %s", got, expected) } ip, err = d.PrivateIPv4() if err != nil { t.Errorf("unknown error") } if got, expected := ip, "10.0.0.1"; got != expected { t.Errorf("Droplet.PrivateIPv4 returned %s; expected %s", got, expected) } ip, err = d.PublicIPv6() if err != nil { t.Errorf("unknown error") } if got, expected := ip, ipv6; got != expected { t.Errorf("Droplet.PublicIPv6 returned %s; expected %s", got, expected) } } godo-1.37.0/errors.go000066400000000000000000000007521366527000000144000ustar00rootroot00000000000000package 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) } godo-1.37.0/errors_test.go000066400000000000000000000003771366527000000154420ustar00rootroot00000000000000package godo import "testing" func TestArgError(t *testing.T) { expected := "foo is invalid because bar" err := NewArgError("foo", "bar") if got := err.Error(); got != expected { t.Errorf("ArgError().Error() = %q; expected %q", got, expected) } } godo-1.37.0/firewalls.go000066400000000000000000000214441366527000000150550ustar00rootroot00000000000000package godo import ( "context" "net/http" "path" "strconv" ) const firewallsBasePath = "/v2/firewalls" // FirewallsService is an interface for managing Firewalls with the DigitalOcean API. // See: https://developers.digitalocean.com/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) } func (fw Firewall) URN() string { return ToURN("Firewall", fw.ID) } // 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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } return root.Firewalls, resp, err } godo-1.37.0/firewalls_test.go000066400000000000000000000454121366527000000161150ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "path" "reflect" "testing" ) var ( firewallCreateJSONBody = ` { "name": "f-i-r-e-w-a-l-l", "inbound_rules": [ { "protocol": "icmp", "sources": { "addresses": ["0.0.0.0/0"], "tags": ["frontend"], "droplet_ids": [123, 456], "load_balancer_uids": ["lb-uid"] } }, { "protocol": "tcp", "ports": "8000-9000", "sources": { "addresses": ["0.0.0.0/0"] } } ], "outbound_rules": [ { "protocol": "icmp", "destinations": { "tags": ["frontend"] } }, { "protocol": "tcp", "ports": "8000-9000", "destinations": { "addresses": ["::/1"] } } ], "droplet_ids": [123], "tags": ["frontend"] } ` firewallRulesJSONBody = ` { "inbound_rules": [ { "protocol": "tcp", "ports": "22", "sources": { "addresses": ["0.0.0.0/0"] } } ], "outbound_rules": [ { "protocol": "tcp", "ports": "443", "destinations": { "addresses": ["0.0.0.0/0"] } } ] } ` firewallUpdateJSONBody = ` { "name": "f-i-r-e-w-a-l-l", "inbound_rules": [ { "protocol": "tcp", "ports": "443", "sources": { "addresses": ["10.0.0.0/8"] } } ], "droplet_ids": [123], "tags": [] } ` firewallUpdateJSONResponse = ` { "firewall": { "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", "name": "f-i-r-e-w-a-l-l", "inbound_rules": [ { "protocol": "tcp", "ports": "443", "sources": { "addresses": ["10.0.0.0/8"] } } ], "outbound_rules": [], "created_at": "2017-04-06T13:07:27Z", "droplet_ids": [ 123 ], "tags": [] } } ` firewallJSONResponse = ` { "firewall": { "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", "name": "f-i-r-e-w-a-l-l", "status": "waiting", "inbound_rules": [ { "protocol": "icmp", "ports": "0", "sources": { "tags": ["frontend"] } }, { "protocol": "tcp", "ports": "8000-9000", "sources": { "addresses": ["0.0.0.0/0"] } } ], "outbound_rules": [ { "protocol": "icmp", "ports": "0" }, { "protocol": "tcp", "ports": "8000-9000", "destinations": { "addresses": ["::/1"] } } ], "created_at": "2017-04-06T13:07:27Z", "droplet_ids": [ 123 ], "tags": [ "frontend" ], "pending_changes": [ { "droplet_id": 123, "removing": false, "status": "waiting" } ] } } ` firewallListJSONResponse = ` { "firewalls": [ { "id": "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", "name": "f-i-r-e-w-a-l-l", "inbound_rules": [ { "protocol": "icmp", "ports": "0", "sources": { "tags": ["frontend"] } }, { "protocol": "tcp", "ports": "8000-9000", "sources": { "addresses": ["0.0.0.0/0"] } } ], "outbound_rules": [ { "protocol": "icmp", "ports": "0" }, { "protocol": "tcp", "ports": "8000-9000", "destinations": { "addresses": ["::/1"] } } ], "created_at": "2017-04-06T13:07:27Z", "droplet_ids": [ 123 ], "tags": [ "frontend" ] } ], "links": {}, "meta": { "total": 1 } } ` ) func TestFirewalls_Get(t *testing.T) { setup() defer teardown() urlStr := "/v2/firewalls" fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr = path.Join(urlStr, fID) mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, firewallJSONResponse) }) actualFirewall, _, err := client.Firewalls.Get(ctx, fID) if err != nil { t.Errorf("Firewalls.Get returned error: %v", err) } expectedFirewall := &Firewall{ ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", Name: "f-i-r-e-w-a-l-l", Status: "waiting", InboundRules: []InboundRule{ { Protocol: "icmp", PortRange: "0", Sources: &Sources{ Tags: []string{"frontend"}, }, }, { Protocol: "tcp", PortRange: "8000-9000", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "icmp", PortRange: "0", }, { Protocol: "tcp", PortRange: "8000-9000", Destinations: &Destinations{ Addresses: []string{"::/1"}, }, }, }, Created: "2017-04-06T13:07:27Z", DropletIDs: []int{123}, Tags: []string{"frontend"}, PendingChanges: []PendingChange{ { DropletID: 123, Removing: false, Status: "waiting", }, }, } if !reflect.DeepEqual(actualFirewall, expectedFirewall) { t.Errorf("Firewalls.Get returned %+v, expected %+v", actualFirewall, expectedFirewall) } } func TestFirewalls_Create(t *testing.T) { setup() defer teardown() expectedFirewallRequest := &FirewallRequest{ Name: "f-i-r-e-w-a-l-l", InboundRules: []InboundRule{ { Protocol: "icmp", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, Tags: []string{"frontend"}, DropletIDs: []int{123, 456}, LoadBalancerUIDs: []string{"lb-uid"}, }, }, { Protocol: "tcp", PortRange: "8000-9000", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "icmp", Destinations: &Destinations{ Tags: []string{"frontend"}, }, }, { Protocol: "tcp", PortRange: "8000-9000", Destinations: &Destinations{ Addresses: []string{"::/1"}, }, }, }, DropletIDs: []int{123}, Tags: []string{"frontend"}, } mux.HandleFunc("/v2/firewalls", func(w http.ResponseWriter, r *http.Request) { v := new(FirewallRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, expectedFirewallRequest) { t.Errorf("Request body = %+v, expected %+v", v, expectedFirewallRequest) } var actualFirewallRequest *FirewallRequest json.Unmarshal([]byte(firewallCreateJSONBody), &actualFirewallRequest) if !reflect.DeepEqual(actualFirewallRequest, expectedFirewallRequest) { t.Errorf("Request body = %+v, expected %+v", actualFirewallRequest, expectedFirewallRequest) } fmt.Fprint(w, firewallJSONResponse) }) actualFirewall, _, err := client.Firewalls.Create(ctx, expectedFirewallRequest) if err != nil { t.Errorf("Firewalls.Create returned error: %v", err) } expectedFirewall := &Firewall{ ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", Name: "f-i-r-e-w-a-l-l", Status: "waiting", InboundRules: []InboundRule{ { Protocol: "icmp", PortRange: "0", Sources: &Sources{ Tags: []string{"frontend"}, }, }, { Protocol: "tcp", PortRange: "8000-9000", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "icmp", PortRange: "0", }, { Protocol: "tcp", PortRange: "8000-9000", Destinations: &Destinations{ Addresses: []string{"::/1"}, }, }, }, Created: "2017-04-06T13:07:27Z", DropletIDs: []int{123}, Tags: []string{"frontend"}, PendingChanges: []PendingChange{ { DropletID: 123, Removing: false, Status: "waiting", }, }, } if !reflect.DeepEqual(actualFirewall, expectedFirewall) { t.Errorf("Firewalls.Create returned %+v, expected %+v", actualFirewall, expectedFirewall) } } func TestFirewalls_Update(t *testing.T) { setup() defer teardown() expectedFirewallRequest := &FirewallRequest{ Name: "f-i-r-e-w-a-l-l", InboundRules: []InboundRule{ { Protocol: "tcp", PortRange: "443", Sources: &Sources{ Addresses: []string{"10.0.0.0/8"}, }, }, }, DropletIDs: []int{123}, Tags: []string{}, } urlStr := "/v2/firewalls" fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr = path.Join(urlStr, fID) mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(FirewallRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, "PUT") if !reflect.DeepEqual(v, expectedFirewallRequest) { t.Errorf("Request body = %+v, expected %+v", v, expectedFirewallRequest) } var actualFirewallRequest *FirewallRequest json.Unmarshal([]byte(firewallUpdateJSONBody), &actualFirewallRequest) if !reflect.DeepEqual(actualFirewallRequest, expectedFirewallRequest) { t.Errorf("Request body = %+v, expected %+v", actualFirewallRequest, expectedFirewallRequest) } fmt.Fprint(w, firewallUpdateJSONResponse) }) actualFirewall, _, err := client.Firewalls.Update(ctx, fID, expectedFirewallRequest) if err != nil { t.Errorf("Firewalls.Update returned error: %v", err) } expectedFirewall := &Firewall{ ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", Name: "f-i-r-e-w-a-l-l", InboundRules: []InboundRule{ { Protocol: "tcp", PortRange: "443", Sources: &Sources{ Addresses: []string{"10.0.0.0/8"}, }, }, }, OutboundRules: []OutboundRule{}, Created: "2017-04-06T13:07:27Z", DropletIDs: []int{123}, Tags: []string{}, } if !reflect.DeepEqual(actualFirewall, expectedFirewall) { t.Errorf("Firewalls.Update returned %+v, expected %+v", actualFirewall, expectedFirewall) } } func TestFirewalls_Delete(t *testing.T) { setup() defer teardown() urlStr := "/v2/firewalls" fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr = path.Join(urlStr, fID) mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Firewalls.Delete(ctx, fID) if err != nil { t.Errorf("Firewalls.Delete returned error: %v", err) } } func TestFirewalls_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/firewalls", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, firewallListJSONResponse) }) actualFirewalls, resp, err := client.Firewalls.List(ctx, nil) if err != nil { t.Errorf("Firewalls.List returned error: %v", err) } expectedFirewalls := makeExpectedFirewalls() if !reflect.DeepEqual(actualFirewalls, expectedFirewalls) { t.Errorf("Firewalls.List returned firewalls %+v, expected %+v", actualFirewalls, expectedFirewalls) } expectedMeta := &Meta{Total: 1} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Firewalls.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestFirewalls_ListByDroplet(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/droplets/123/firewalls", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, firewallListJSONResponse) }) actualFirewalls, resp, err := client.Firewalls.ListByDroplet(ctx, 123, nil) if err != nil { t.Errorf("Firewalls.List returned error: %v", err) } expectedFirewalls := makeExpectedFirewalls() if !reflect.DeepEqual(actualFirewalls, expectedFirewalls) { t.Errorf("Firewalls.List returned firewalls %+v, expected %+v", actualFirewalls, expectedFirewalls) } expectedMeta := &Meta{Total: 1} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Firewalls.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestFirewalls_AddDroplets(t *testing.T) { setup() defer teardown() dRequest := &dropletsRequest{ IDs: []int{123}, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "droplets") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(dropletsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, dRequest) { t.Errorf("Request body = %+v, expected %+v", v, dRequest) } expectedJSONBody := `{"droplet_ids": [123]}` var actualDropletsRequest *dropletsRequest json.Unmarshal([]byte(expectedJSONBody), &actualDropletsRequest) if !reflect.DeepEqual(actualDropletsRequest, dRequest) { t.Errorf("Request body = %+v, expected %+v", actualDropletsRequest, dRequest) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.AddDroplets(ctx, fID, dRequest.IDs...) if err != nil { t.Errorf("Firewalls.AddDroplets returned error: %v", err) } } func TestFirewalls_RemoveDroplets(t *testing.T) { setup() defer teardown() dRequest := &dropletsRequest{ IDs: []int{123, 345}, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "droplets") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(dropletsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodDelete) if !reflect.DeepEqual(v, dRequest) { t.Errorf("Request body = %+v, expected %+v", v, dRequest) } expectedJSONBody := `{"droplet_ids": [123, 345]}` var actualDropletsRequest *dropletsRequest json.Unmarshal([]byte(expectedJSONBody), &actualDropletsRequest) if !reflect.DeepEqual(actualDropletsRequest, dRequest) { t.Errorf("Request body = %+v, expected %+v", actualDropletsRequest, dRequest) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.RemoveDroplets(ctx, fID, dRequest.IDs...) if err != nil { t.Errorf("Firewalls.RemoveDroplets returned error: %v", err) } } func TestFirewalls_AddTags(t *testing.T) { setup() defer teardown() tRequest := &tagsRequest{ Tags: []string{"frontend"}, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "tags") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(tagsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, tRequest) { t.Errorf("Request body = %+v, expected %+v", v, tRequest) } var actualTagsRequest *tagsRequest json.Unmarshal([]byte(`{"tags": ["frontend"]}`), &actualTagsRequest) if !reflect.DeepEqual(actualTagsRequest, tRequest) { t.Errorf("Request body = %+v, expected %+v", actualTagsRequest, tRequest) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.AddTags(ctx, fID, tRequest.Tags...) if err != nil { t.Errorf("Firewalls.AddTags returned error: %v", err) } } func TestFirewalls_RemoveTags(t *testing.T) { setup() defer teardown() tRequest := &tagsRequest{ Tags: []string{"frontend", "backend"}, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "tags") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(tagsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodDelete) if !reflect.DeepEqual(v, tRequest) { t.Errorf("Request body = %+v, expected %+v", v, tRequest) } var actualTagsRequest *tagsRequest json.Unmarshal([]byte(`{"tags": ["frontend", "backend"]}`), &actualTagsRequest) if !reflect.DeepEqual(actualTagsRequest, tRequest) { t.Errorf("Request body = %+v, expected %+v", actualTagsRequest, tRequest) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.RemoveTags(ctx, fID, tRequest.Tags...) if err != nil { t.Errorf("Firewalls.RemoveTags returned error: %v", err) } } func TestFirewalls_AddRules(t *testing.T) { setup() defer teardown() rr := &FirewallRulesRequest{ InboundRules: []InboundRule{ { Protocol: "tcp", PortRange: "22", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "tcp", PortRange: "443", Destinations: &Destinations{ Addresses: []string{"0.0.0.0/0"}, }, }, }, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "rules") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(FirewallRulesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, rr) { t.Errorf("Request body = %+v, expected %+v", v, rr) } var actualFirewallRulesRequest *FirewallRulesRequest json.Unmarshal([]byte(firewallRulesJSONBody), &actualFirewallRulesRequest) if !reflect.DeepEqual(actualFirewallRulesRequest, rr) { t.Errorf("Request body = %+v, expected %+v", actualFirewallRulesRequest, rr) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.AddRules(ctx, fID, rr) if err != nil { t.Errorf("Firewalls.AddRules returned error: %v", err) } } func TestFirewalls_RemoveRules(t *testing.T) { setup() defer teardown() rr := &FirewallRulesRequest{ InboundRules: []InboundRule{ { Protocol: "tcp", PortRange: "22", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "tcp", PortRange: "443", Destinations: &Destinations{ Addresses: []string{"0.0.0.0/0"}, }, }, }, } fID := "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0" urlStr := path.Join("/v2/firewalls", fID, "rules") mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) { v := new(FirewallRulesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodDelete) if !reflect.DeepEqual(v, rr) { t.Errorf("Request body = %+v, expected %+v", v, rr) } var actualFirewallRulesRequest *FirewallRulesRequest json.Unmarshal([]byte(firewallRulesJSONBody), &actualFirewallRulesRequest) if !reflect.DeepEqual(actualFirewallRulesRequest, rr) { t.Errorf("Request body = %+v, expected %+v", actualFirewallRulesRequest, rr) } fmt.Fprint(w, nil) }) _, err := client.Firewalls.RemoveRules(ctx, fID, rr) if err != nil { t.Errorf("Firewalls.RemoveRules returned error: %v", err) } } func makeExpectedFirewalls() []Firewall { return []Firewall{ Firewall{ ID: "fe6b88f2-b42b-4bf7-bbd3-5ae20208f0b0", Name: "f-i-r-e-w-a-l-l", InboundRules: []InboundRule{ { Protocol: "icmp", PortRange: "0", Sources: &Sources{ Tags: []string{"frontend"}, }, }, { Protocol: "tcp", PortRange: "8000-9000", Sources: &Sources{ Addresses: []string{"0.0.0.0/0"}, }, }, }, OutboundRules: []OutboundRule{ { Protocol: "icmp", PortRange: "0", }, { Protocol: "tcp", PortRange: "8000-9000", Destinations: &Destinations{ Addresses: []string{"::/1"}, }, }, }, DropletIDs: []int{123}, Tags: []string{"frontend"}, Created: "2017-04-06T13:07:27Z", }, } } godo-1.37.0/floating_ips.go000066400000000000000000000072001366527000000155350ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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) } func (f FloatingIP) URN() string { return ToURN("FloatingIP", f.IP) } type floatingIPsRoot struct { FloatingIPs []FloatingIP `json:"floating_ips"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } godo-1.37.0/floating_ips_actions.go000066400000000000000000000062631366527000000172650ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) // 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) } godo-1.37.0/floating_ips_actions_test.go000066400000000000000000000107721366527000000203240ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestFloatingIPsActions_Assign(t *testing.T) { setup() defer teardown() dropletID := 12345 assignRequest := &ActionRequest{ "droplet_id": float64(dropletID), // encoding/json decodes numbers as floats "type": "assign", } mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, assignRequest) { t.Errorf("Request body = %#v, expected %#v", v, assignRequest) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) assign, _, err := client.FloatingIPActions.Assign(ctx, "192.168.0.1", 12345) if err != nil { t.Errorf("FloatingIPsActions.Assign returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(assign, expected) { t.Errorf("FloatingIPsActions.Assign returned %+v, expected %+v", assign, expected) } } func TestFloatingIPsActions_Unassign(t *testing.T) { setup() defer teardown() unassignRequest := &ActionRequest{ "type": "unassign", } mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, unassignRequest) { t.Errorf("Request body = %+v, expected %+v", v, unassignRequest) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.FloatingIPActions.Unassign(ctx, "192.168.0.1") if err != nil { t.Errorf("FloatingIPsActions.Get returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("FloatingIPsActions.Get returned %+v, expected %+v", action, expected) } } func TestFloatingIPsActions_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions/456", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.FloatingIPActions.Get(ctx, "192.168.0.1", 456) if err != nil { t.Errorf("FloatingIPsActions.Get returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("FloatingIPsActions.Get returned %+v, expected %+v", action, expected) } } func TestFloatingIPsActions_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{"actions":[{"status":"in-progress"}]}`) }) actions, _, err := client.FloatingIPActions.List(ctx, "192.168.0.1", nil) if err != nil { t.Errorf("FloatingIPsActions.List returned error: %v", err) } expected := []Action{{Status: "in-progress"}} if !reflect.DeepEqual(actions, expected) { t.Errorf("FloatingIPsActions.List returned %+v, expected %+v", actions, expected) } } func TestFloatingIPsActions_ListMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"actions":[{"status":"in-progress"}], "links":{"pages":{"next":"http://example.com/v2/floating_ips/192.168.0.1/actions?page=2"}}}`) }) _, resp, err := client.FloatingIPActions.List(ctx, "192.168.0.1", nil) if err != nil { t.Errorf("FloatingIPsActions.List returned error: %v", err) } checkCurrentPage(t, resp, 1) } func TestFloatingIPsActions_ListPageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "actions":[{"status":"in-progress"}], "links":{ "pages":{ "next":"http://example.com/v2/regions/?page=3", "prev":"http://example.com/v2/regions/?page=1", "last":"http://example.com/v2/regions/?page=3", "first":"http://example.com/v2/regions/?page=1" } } }` mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.FloatingIPActions.List(ctx, "192.168.0.1", opt) if err != nil { t.Errorf("FloatingIPsActions.List returned error: %v", err) } checkCurrentPage(t, resp, 2) } godo-1.37.0/floating_ips_test.go000066400000000000000000000107401366527000000165770ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestFloatingIPs_ListFloatingIPs(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}],"meta":{"total":2}}`) }) floatingIPs, resp, err := client.FloatingIPs.List(ctx, nil) if err != nil { t.Errorf("FloatingIPs.List returned error: %v", err) } expectedFloatingIPs := []FloatingIP{ {Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"}, {Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 2}, IP: "192.168.0.2"}, } if !reflect.DeepEqual(floatingIPs, expectedFloatingIPs) { t.Errorf("FloatingIPs.List returned floating IPs %+v, expected %+v", floatingIPs, expectedFloatingIPs) } expectedMeta := &Meta{ Total: 2, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("FloatingIPs.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestFloatingIPs_ListFloatingIPsMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}], "links":{"pages":{"next":"http://example.com/v2/floating_ips/?page=2"}}}`) }) _, resp, err := client.FloatingIPs.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestFloatingIPs_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "floating_ips": [{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"},{"region":{"slug":"nyc3"},"droplet":{"id":2},"ip":"192.168.0.2"}], "links":{ "pages":{ "next":"http://example.com/v2/floating_ips/?page=3", "prev":"http://example.com/v2/floating_ips/?page=1", "last":"http://example.com/v2/floating_ips/?page=3", "first":"http://example.com/v2/floating_ips/?page=1" } } }` mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.FloatingIPs.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestFloatingIPs_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips/192.168.0.1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`) }) floatingIP, _, err := client.FloatingIPs.Get(ctx, "192.168.0.1") if err != nil { t.Errorf("domain.Get returned error: %v", err) } expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"} if !reflect.DeepEqual(floatingIP, expected) { t.Errorf("FloatingIPs.Get returned %+v, expected %+v", floatingIP, expected) } } func TestFloatingIPs_Create(t *testing.T) { setup() defer teardown() createRequest := &FloatingIPCreateRequest{ Region: "nyc3", DropletID: 1, } mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) { v := new(FloatingIPCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, `{"floating_ip":{"region":{"slug":"nyc3"},"droplet":{"id":1},"ip":"192.168.0.1"}}`) }) floatingIP, _, err := client.FloatingIPs.Create(ctx, createRequest) if err != nil { t.Errorf("FloatingIPs.Create returned error: %v", err) } expected := &FloatingIP{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"} if !reflect.DeepEqual(floatingIP, expected) { t.Errorf("FloatingIPs.Create returned %+v, expected %+v", floatingIP, expected) } } func TestFloatingIPs_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/floating_ips/192.168.0.1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.FloatingIPs.Delete(ctx, "192.168.0.1") if err != nil { t.Errorf("FloatingIPs.Delete returned error: %v", err) } } godo-1.37.0/go.mod000066400000000000000000000007731366527000000136460ustar00rootroot00000000000000module github.com/digitalocean/godo go 1.14 require ( github.com/golang/protobuf v1.3.5 // indirect github.com/google/go-querystring v1.0.0 github.com/stretchr/testify v1.4.0 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d google.golang.org/appengine v1.6.5 // indirect ) replace github.com/stretchr/objx => github.com/stretchr/objx v0.2.0 replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a godo-1.37.0/go.sum000066400000000000000000000075511366527000000136740ustar00rootroot00000000000000cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= godo-1.37.0/godo.go000066400000000000000000000274411366527000000140200ustar00rootroot00000000000000package godo import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "strconv" "time" "github.com/google/go-querystring/query" "golang.org/x/oauth2" ) const ( libraryVersion = "1.37.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 Balance BalanceService BillingHistory BillingHistoryService CDNs CDNService Domains DomainsService Droplets DropletsService DropletActions DropletActionsService Images ImagesService ImageActions ImageActionsService Invoices InvoicesService Keys KeysService Regions RegionsService Sizes SizesService FloatingIPs FloatingIPsService FloatingIPActions FloatingIPActionsService Snapshots SnapshotsService Storage StorageService StorageActions StorageActionsService Tags TagsService LoadBalancers LoadBalancersService Certificates CertificatesService Firewalls FirewallsService Projects ProjectsService Kubernetes KubernetesService Registry RegistryService Databases DatabasesService VPCs VPCsService OneClick OneClickService // 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 // Meta describes generic information about the response. Meta *Meta // 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 } // NewFromToken returns a new DigitalOcean API client with the given API // token. func NewFromToken(token string) *Client { ctx := context.Background() config := &oauth2.Config{} ts := config.TokenSource(ctx, &oauth2.Token{AccessToken: token}) return NewClient(oauth2.NewClient(ctx, ts)) } // NewClient returns a new DigitalOcean API client, using the given // http.Client to perform all requests. // // Users who wish to pass their own http.Client should use this method. If // you're in need of further customization, the godo.New method allows more // options, such as setting a custom URL or a custom user agent string. 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.Balance = &BalanceServiceOp{client: c} c.BillingHistory = &BillingHistoryServiceOp{client: c} c.CDNs = &CDNServiceOp{client: c} c.Certificates = &CertificatesServiceOp{client: c} c.Domains = &DomainsServiceOp{client: c} c.Droplets = &DropletsServiceOp{client: c} c.DropletActions = &DropletActionsServiceOp{client: c} c.Firewalls = &FirewallsServiceOp{client: c} c.FloatingIPs = &FloatingIPsServiceOp{client: c} c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c} c.Images = &ImagesServiceOp{client: c} c.ImageActions = &ImageActionsServiceOp{client: c} c.Invoices = &InvoicesServiceOp{client: c} c.Keys = &KeysServiceOp{client: c} c.LoadBalancers = &LoadBalancersServiceOp{client: c} c.Projects = &ProjectsServiceOp{client: c} c.Regions = &RegionsServiceOp{client: c} c.Sizes = &SizesServiceOp{client: c} c.Snapshots = &SnapshotsServiceOp{client: c} c.Storage = &StorageServiceOp{client: c} c.StorageActions = &StorageActionsServiceOp{client: c} c.Tags = &TagsServiceOp{client: c} c.Kubernetes = &KubernetesServiceOp{client: c} c.Registry = &RegistryServiceOp{client: c} c.Databases = &DatabasesServiceOp{client: c} c.VPCs = &VPCsServiceOp{client: c} c.OneClick = &OneClickServiceOp{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) { u, err := c.BaseURL.Parse(urlStr) if err != nil { return nil, err } 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 } // 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 := 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 } // DoRequest submits an HTTP request. func DoRequest(ctx context.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.Context, client *http.Client, req *http.Request) (*http.Response, error) { req = req.WithContext(ctx) return client.Do(req) } 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 { errorResponse.Message = string(data) } } 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() } godo-1.37.0/godo_test.go000066400000000000000000000324101366527000000150470ustar00rootroot00000000000000package godo import ( "context" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "reflect" "strings" "testing" "time" ) var ( mux *http.ServeMux ctx = context.TODO() client *Client server *httptest.Server ) func setup() { mux = http.NewServeMux() server = httptest.NewServer(mux) client = NewClient(nil) url, _ := url.Parse(server.URL) client.BaseURL = url } func teardown() { server.Close() } func testMethod(t *testing.T, r *http.Request, expected string) { if expected != r.Method { t.Errorf("Request method = %v, expected %v", r.Method, expected) } } type values map[string]string func testFormValues(t *testing.T, r *http.Request, values values) { expected := url.Values{} for k, v := range values { expected.Add(k, v) } err := r.ParseForm() if err != nil { t.Fatalf("parseForm(): %v", err) } if !reflect.DeepEqual(expected, r.Form) { t.Errorf("Request parameters = %v, expected %v", r.Form, expected) } } func testURLParseError(t *testing.T, err error) { if err == nil { t.Errorf("Expected error to be returned") } if err, ok := err.(*url.Error); !ok || err.Op != "parse" { t.Errorf("Expected URL parse error, got %+v", err) } } func testClientServices(t *testing.T, c *Client) { services := []string{ "Account", "Actions", "Balance", "BillingHistory", "CDNs", "Domains", "Droplets", "DropletActions", "Images", "ImageActions", "Invoices", "Keys", "Regions", "Sizes", "FloatingIPs", "FloatingIPActions", "Tags", } cp := reflect.ValueOf(c) cv := reflect.Indirect(cp) for _, s := range services { if cv.FieldByName(s).IsNil() { t.Errorf("c.%s shouldn't be nil", s) } } } func testClientDefaultBaseURL(t *testing.T, c *Client) { if c.BaseURL == nil || c.BaseURL.String() != defaultBaseURL { t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL, defaultBaseURL) } } func testClientDefaultUserAgent(t *testing.T, c *Client) { if c.UserAgent != userAgent { t.Errorf("NewClient UserAgent = %v, expected %v", c.UserAgent, userAgent) } } func testClientDefaults(t *testing.T, c *Client) { testClientDefaultBaseURL(t, c) testClientDefaultUserAgent(t, c) testClientServices(t, c) } func TestNewClient(t *testing.T) { c := NewClient(nil) testClientDefaults(t, c) } func TestNewFromToken(t *testing.T) { c := NewFromToken("my-token") testClientDefaults(t, c) } func TestNew(t *testing.T) { c, err := New(nil) if err != nil { t.Fatalf("New(): %v", err) } testClientDefaults(t, c) } func TestNewRequest(t *testing.T) { c := NewClient(nil) inURL, outURL := "/foo", defaultBaseURL+"foo" inBody, outBody := &DropletCreateRequest{Name: "l"}, `{"name":"l","region":"","size":"","image":0,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+ `"private_networking":false,"monitoring":false,"tags":null}`+"\n" req, _ := c.NewRequest(ctx, http.MethodGet, inURL, inBody) // test relative URL was expanded if req.URL.String() != outURL { t.Errorf("NewRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL) } // test body was JSON encoded body, _ := ioutil.ReadAll(req.Body) if string(body) != outBody { t.Errorf("NewRequest(%v)Body = %v, expected %v", inBody, string(body), outBody) } // test default user-agent is attached to the request userAgent := req.Header.Get("User-Agent") if c.UserAgent != userAgent { t.Errorf("NewRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent) } } func TestNewRequest_withUserData(t *testing.T) { c := NewClient(nil) inURL, outURL := "/foo", defaultBaseURL+"foo" inBody, outBody := &DropletCreateRequest{Name: "l", UserData: "u"}, `{"name":"l","region":"","size":"","image":0,`+ `"ssh_keys":null,"backups":false,"ipv6":false,`+ `"private_networking":false,"monitoring":false,"user_data":"u","tags":null}`+"\n" req, _ := c.NewRequest(ctx, http.MethodGet, inURL, inBody) // test relative URL was expanded if req.URL.String() != outURL { t.Errorf("NewRequest(%v) URL = %v, expected %v", inURL, req.URL, outURL) } // test body was JSON encoded body, _ := ioutil.ReadAll(req.Body) if string(body) != outBody { t.Errorf("NewRequest(%v)Body = %v, expected %v", inBody, string(body), outBody) } // test default user-agent is attached to the request userAgent := req.Header.Get("User-Agent") if c.UserAgent != userAgent { t.Errorf("NewRequest() User-Agent = %v, expected %v", userAgent, c.UserAgent) } } func TestNewRequest_badURL(t *testing.T) { c := NewClient(nil) _, err := c.NewRequest(ctx, http.MethodGet, ":", nil) testURLParseError(t, err) } func TestNewRequest_withCustomUserAgent(t *testing.T) { ua := "testing/0.0.1" c, err := New(nil, SetUserAgent(ua)) if err != nil { t.Fatalf("New() unexpected error: %v", err) } req, _ := c.NewRequest(ctx, http.MethodGet, "/foo", nil) expected := fmt.Sprintf("%s %s", ua, userAgent) if got := req.Header.Get("User-Agent"); got != expected { t.Errorf("New() UserAgent = %s; expected %s", got, expected) } } func TestDo(t *testing.T) { setup() defer teardown() type foo struct { A string } mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if m := http.MethodGet; m != r.Method { t.Errorf("Request method = %v, expected %v", r.Method, m) } fmt.Fprint(w, `{"A":"a"}`) }) req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) body := new(foo) _, err := client.Do(context.Background(), req, body) if err != nil { t.Fatalf("Do(): %v", err) } expected := &foo{"a"} if !reflect.DeepEqual(body, expected) { t.Errorf("Response body = %v, expected %v", body, expected) } } func TestDo_httpError(t *testing.T) { setup() defer teardown() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad Request", 400) }) req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) _, err := client.Do(context.Background(), req, nil) if err == nil { t.Error("Expected HTTP 400 error.") } } // Test handling of an error caused by the internal http client's Do() // function. func TestDo_redirectLoop(t *testing.T) { setup() defer teardown() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) }) req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) _, err := client.Do(context.Background(), req, nil) if err == nil { t.Error("Expected error to be returned.") } if err, ok := err.(*url.Error); !ok { t.Errorf("Expected a URL error; got %#v.", err) } } func TestCheckResponse(t *testing.T) { res := &http.Response{ Request: &http.Request{}, StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(strings.NewReader(`{"message":"m", "errors": [{"resource": "r", "field": "f", "code": "c"}]}`)), } err := CheckResponse(res).(*ErrorResponse) if err == nil { t.Fatalf("Expected error response.") } expected := &ErrorResponse{ Response: res, Message: "m", } if !reflect.DeepEqual(err, expected) { t.Errorf("Error = %#v, expected %#v", err, expected) } } // ensure that we properly handle API errors that do not contain a response // body func TestCheckResponse_noBody(t *testing.T) { res := &http.Response{ Request: &http.Request{}, StatusCode: http.StatusBadRequest, Body: ioutil.NopCloser(strings.NewReader("")), } err := CheckResponse(res).(*ErrorResponse) if err == nil { t.Errorf("Expected error response.") } expected := &ErrorResponse{ Response: res, } if !reflect.DeepEqual(err, expected) { t.Errorf("Error = %#v, expected %#v", err, expected) } } func TestErrorResponse_Error(t *testing.T) { res := &http.Response{Request: &http.Request{}} err := ErrorResponse{Message: "m", Response: res} if err.Error() == "" { t.Errorf("Expected non-empty ErrorResponse.Error()") } } func TestDo_rateLimit(t *testing.T) { setup() defer teardown() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Add(headerRateLimit, "60") w.Header().Add(headerRateRemaining, "59") w.Header().Add(headerRateReset, "1372700873") }) var expected int if expected = 0; client.Rate.Limit != expected { t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected) } if expected = 0; client.Rate.Remaining != expected { t.Errorf("Client rate remaining = %v, got %v", client.Rate.Remaining, expected) } if !client.Rate.Reset.IsZero() { t.Errorf("Client rate reset not initialized to zero value") } req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) _, err := client.Do(context.Background(), req, nil) if err != nil { t.Fatalf("Do(): %v", err) } if expected = 60; client.Rate.Limit != expected { t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected) } if expected = 59; client.Rate.Remaining != expected { t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected) } reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC) if client.Rate.Reset.UTC() != reset { t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset) } } func TestDo_rateLimit_errorResponse(t *testing.T) { setup() defer teardown() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Add(headerRateLimit, "60") w.Header().Add(headerRateRemaining, "59") w.Header().Add(headerRateReset, "1372700873") http.Error(w, `{"message":"bad request"}`, 400) }) var expected int req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) _, _ = client.Do(context.Background(), req, nil) if expected = 60; client.Rate.Limit != expected { t.Errorf("Client rate limit = %v, expected %v", client.Rate.Limit, expected) } if expected = 59; client.Rate.Remaining != expected { t.Errorf("Client rate remaining = %v, expected %v", client.Rate.Remaining, expected) } reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC) if client.Rate.Reset.UTC() != reset { t.Errorf("Client rate reset = %v, expected %v", client.Rate.Reset, reset) } } func checkCurrentPage(t *testing.T, resp *Response, expectedPage int) { links := resp.Links p, err := links.CurrentPage() if err != nil { t.Fatal(err) } if p != expectedPage { t.Fatalf("expected current page to be '%d', was '%d'", expectedPage, p) } } func TestDo_completion_callback(t *testing.T) { setup() defer teardown() type foo struct { A string } mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if m := http.MethodGet; m != r.Method { t.Errorf("Request method = %v, expected %v", r.Method, m) } fmt.Fprint(w, `{"A":"a"}`) }) req, _ := client.NewRequest(ctx, http.MethodGet, "/", nil) body := new(foo) var completedReq *http.Request var completedResp string client.OnRequestCompleted(func(req *http.Request, resp *http.Response) { completedReq = req b, err := httputil.DumpResponse(resp, true) if err != nil { t.Errorf("Failed to dump response: %s", err) } completedResp = string(b) }) _, err := client.Do(context.Background(), req, body) if err != nil { t.Fatalf("Do(): %v", err) } if !reflect.DeepEqual(req, completedReq) { t.Errorf("Completed request = %v, expected %v", completedReq, req) } expected := `{"A":"a"}` if !strings.Contains(completedResp, expected) { t.Errorf("expected response to contain %v, Response = %v", expected, completedResp) } } func TestAddOptions(t *testing.T) { cases := []struct { name string path string expected string opts *ListOptions isErr bool }{ { name: "add options", path: "/action", expected: "/action?page=1", opts: &ListOptions{Page: 1}, isErr: false, }, { name: "add options with existing parameters", path: "/action?scope=all", expected: "/action?page=1&scope=all", opts: &ListOptions{Page: 1}, isErr: false, }, } for _, c := range cases { got, err := addOptions(c.path, c.opts) if c.isErr && err == nil { t.Errorf("%q expected error but none was encountered", c.name) continue } if !c.isErr && err != nil { t.Errorf("%q unexpected error: %v", c.name, err) continue } gotURL, err := url.Parse(got) if err != nil { t.Errorf("%q unable to parse returned URL", c.name) continue } expectedURL, err := url.Parse(c.expected) if err != nil { t.Errorf("%q unable to parse expected URL", c.name) continue } if g, e := gotURL.Path, expectedURL.Path; g != e { t.Errorf("%q path = %q; expected %q", c.name, g, e) continue } if g, e := gotURL.Query(), expectedURL.Query(); !reflect.DeepEqual(g, e) { t.Errorf("%q query = %#v; expected %#v", c.name, g, e) continue } } } func TestCustomUserAgent(t *testing.T) { ua := "testing/0.0.1" c, err := New(nil, SetUserAgent(ua)) if err != nil { t.Fatalf("New() unexpected error: %v", err) } expected := fmt.Sprintf("%s %s", ua, userAgent) if got := c.UserAgent; got != expected { t.Errorf("New() UserAgent = %s; expected %s", got, expected) } } func TestCustomBaseURL(t *testing.T) { baseURL := "http://localhost/foo" c, err := New(nil, SetBaseURL(baseURL)) if err != nil { t.Fatalf("New() unexpected error: %v", err) } expected := baseURL if got := c.BaseURL.String(); got != expected { t.Errorf("New() BaseURL = %s; expected %s", got, expected) } } func TestCustomBaseURL_badURL(t *testing.T) { baseURL := ":" _, err := New(nil, SetBaseURL(baseURL)) testURLParseError(t, err) } func intPtr(val int) *int { return &val } func boolPtr(val bool) *bool { return &val } godo-1.37.0/image_actions.go000066400000000000000000000051151366527000000156640ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) // 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 } godo-1.37.0/image_actions_test.go000066400000000000000000000045641366527000000167320ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestImageActions_Transfer(t *testing.T) { setup() defer teardown() transferRequest := &ActionRequest{} mux.HandleFunc("/v2/images/12345/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, transferRequest) { t.Errorf("Request body = %+v, expected %+v", v, transferRequest) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) transfer, _, err := client.ImageActions.Transfer(ctx, 12345, transferRequest) if err != nil { t.Errorf("ImageActions.Transfer returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(transfer, expected) { t.Errorf("ImageActions.Transfer returned %+v, expected %+v", transfer, expected) } } func TestImageActions_Convert(t *testing.T) { setup() defer teardown() convertRequest := &ActionRequest{ "type": "convert", } mux.HandleFunc("/v2/images/12345/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, convertRequest) { t.Errorf("Request body = %+v, expected %+v", v, convertRequest) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) transfer, _, err := client.ImageActions.Convert(ctx, 12345) if err != nil { t.Errorf("ImageActions.Transfer returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(transfer, expected) { t.Errorf("ImageActions.Transfer returned %+v, expected %+v", transfer, expected) } } func TestImageActions_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images/123/actions/456", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.ImageActions.Get(ctx, 123, 456) if err != nil { t.Errorf("ImageActions.Get returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("ImageActions.Get returned %+v, expected %+v", action, expected) } } godo-1.37.0/images.go000066400000000000000000000161731366527000000143350ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) GetByID(context.Context, int) (*Image, *Response, error) GetBySlug(context.Context, string) (*Image, *Response, error) Create(context.Context, *CustomImageCreateRequest) (*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"` SizeGigaBytes float64 `json:"size_gigabytes,omitempty"` Created string `json:"created_at,omitempty"` Description string `json:"description,omitempty"` Tags []string `json:"tags,omitempty"` Status string `json:"status,omitempty"` ErrorMessage string `json:"error_message,omitempty"` } // ImageUpdateRequest represents a request to update an image. type ImageUpdateRequest struct { Name string `json:"name"` } // CustomImageCreateRequest represents a request to create a custom image. type CustomImageCreateRequest struct { Name string `json:"name"` Url string `json:"url"` Region string `json:"region"` Distribution string `json:"distribution,omitempty"` Description string `json:"description,omitempty"` Tags []string `json:"tags,omitempty"` } type imageRoot struct { Image *Image } type imagesRoot struct { Images []Image Links *Links `json:"links"` Meta *Meta `json:"meta"` } type listImageOptions struct { Private bool `url:"private,omitempty"` Type string `url:"type,omitempty"` Tag string `url:"tag_name,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) } // ListByTag lists all images with a specific tag applied. func (s *ImagesServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) { listOpt := listImageOptions{Tag: tag} 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)) } func (s *ImagesServiceOp) Create(ctx context.Context, createRequest *CustomImageCreateRequest) (*Image, *Response, error) { if createRequest == nil { return nil, nil, NewArgError("createRequest", "cannot be nil") } req, err := s.client.NewRequest(ctx, http.MethodPost, imageBasePath, createRequest) 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 } // 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, http.MethodPut, 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 } if m := root.Meta; m != nil { resp.Meta = m } return root.Images, resp, err } godo-1.37.0/images_test.go000066400000000000000000000237231366527000000153730ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestImages_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "images": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) images, resp, err := client.Images.List(ctx, nil) if err != nil { t.Errorf("Images.List returned error: %v", err) } expectedImages := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(images, expectedImages) { t.Errorf("Images.List returned images %+v, expected %+v", images, expectedImages) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Images.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestImages_ListDistribution(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "distribution" actual := r.URL.Query().Get("type") if actual != expected { t.Errorf("'type' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{ "images": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) images, resp, err := client.Images.ListDistribution(ctx, nil) if err != nil { t.Errorf("Images.ListDistribution returned error: %v", err) } expectedImages := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(images, expectedImages) { t.Errorf("Images.ListDistribution returned images %+v, expected %+v", images, expectedImages) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Images.ListDistribution returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestImages_ListApplication(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "application" actual := r.URL.Query().Get("type") if actual != expected { t.Errorf("'type' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{ "images": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) images, resp, err := client.Images.ListApplication(ctx, nil) if err != nil { t.Errorf("Images.ListApplication returned error: %v", err) } expectedImages := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(images, expectedImages) { t.Errorf("Images.ListApplication returned images %+v, expected %+v", images, expectedImages) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Images.ListApplication returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestImages_ListUser(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "true" actual := r.URL.Query().Get("private") if actual != expected { t.Errorf("'private' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{ "images": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) images, resp, err := client.Images.ListUser(ctx, nil) if err != nil { t.Errorf("Images.ListUser returned error: %v", err) } expectedImages := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(images, expectedImages) { t.Errorf("Images.ListUser returned images %+v, expected %+v", images, expectedImages) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Images.ListUser returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestImages_ListByTag(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "foo" actual := r.URL.Query().Get("tag_name") if actual != expected { t.Errorf("'tag_name' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{ "images": [ { "id": 1 }, { "id":2 } ], "meta": { "total": 2 } }`) }) images, resp, err := client.Images.ListByTag(ctx, "foo", nil) if err != nil { t.Errorf("Images.ListByTag returned error: %v", err) } expectedImages := []Image{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(images, expectedImages) { t.Errorf("Images.ListByTag returned images %+v, expected %+v", images, expectedImages) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Images.ListByTag returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestImages_ListImagesMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"images": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/images/?page=2"}}}`) }) _, resp, err := client.Images.List(ctx, &ListOptions{Page: 2}) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestImages_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "images": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/images/?page=3", "prev":"http://example.com/v2/images/?page=1", "last":"http://example.com/v2/images/?page=3", "first":"http://example.com/v2/images/?page=1" } } }` mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Images.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestImages_GetImageByID(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"image":{"id":12345}}`) }) images, _, err := client.Images.GetByID(ctx, 12345) if err != nil { t.Errorf("Image.GetByID returned error: %v", err) } expected := &Image{ID: 12345} if !reflect.DeepEqual(images, expected) { t.Errorf("Images.GetByID returned %+v, expected %+v", images, expected) } } func TestImages_GetImageBySlug(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images/ubuntu", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"image":{"id":12345}}`) }) images, _, err := client.Images.GetBySlug(ctx, "ubuntu") if err != nil { t.Errorf("Image.GetBySlug returned error: %v", err) } expected := &Image{ID: 12345} if !reflect.DeepEqual(images, expected) { t.Errorf("Images.Get returned %+v, expected %+v", images, expected) } } func TestImages_Create(t *testing.T) { setup() defer teardown() createRequest := &CustomImageCreateRequest{ Name: "my-new-image", Url: "http://example.com/distro-amd64.img", Region: "nyc3", Distribution: "Ubuntu", Description: "My new custom image", Tags: []string{"foo", "bar"}, } mux.HandleFunc("/v2/images", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "name": "my-new-image", "url": "http://example.com/distro-amd64.img", "region": "nyc3", "distribution": "Ubuntu", "description": "My new custom image", "tags": []interface{}{"foo", "bar"}, } var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected) } fmt.Fprintf(w, `{"image": {"id": 1,"created_at": "2018-09-20T19:28:00Z","description": "A custom image","distribution": "Ubuntu","error_message": "","regions": [],"type": "custom","tags":["foo","bar"],"status": "NEW"}}`) }) image, _, err := client.Images.Create(ctx, createRequest) if err != nil { t.Errorf("Images.Create returned error: %v", err) } if id := image.ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } } func TestImages_Update(t *testing.T) { setup() defer teardown() updateRequest := &ImageUpdateRequest{ Name: "name", } mux.HandleFunc("/v2/images/12345", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "name": "name", } var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body = %#v, expected %#v", v, expected) } fmt.Fprintf(w, `{"image":{"id":1}}`) }) image, _, err := client.Images.Update(ctx, 12345, updateRequest) if err != nil { t.Errorf("Images.Update returned error: %v", err) } else { if id := image.ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } } } func TestImages_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/images/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Images.Delete(ctx, 12345) if err != nil { t.Errorf("Image.Delete returned error: %v", err) } } func TestImage_String(t *testing.T) { image := &Image{ ID: 1, Name: "Image", Type: "snapshot", Distribution: "Ubuntu", Slug: "image", Public: true, Regions: []string{"one", "two"}, MinDiskSize: 20, SizeGigaBytes: 2.36, Created: "2013-11-27T09:24:55Z", } stringified := image.String() expected := `godo.Image{ID:1, Name:"Image", Type:"snapshot", Distribution:"Ubuntu", Slug:"image", Public:true, Regions:["one" "two"], MinDiskSize:20, SizeGigaBytes:2.36, Created:"2013-11-27T09:24:55Z", Description:"", Status:"", ErrorMessage:""}` if expected != stringified { t.Errorf("Image.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/invoices.go000066400000000000000000000155661366527000000147140ustar00rootroot00000000000000package godo import ( "bytes" "context" "fmt" "net/http" "time" ) const invoicesBasePath = "v2/customers/my/invoices" // InvoicesService is an interface for interfacing with the Invoice // endpoints of the DigitalOcean API // See: https://developers.digitalocean.com/documentation/v2/#invoices type InvoicesService interface { Get(context.Context, string, *ListOptions) (*Invoice, *Response, error) GetPDF(context.Context, string) ([]byte, *Response, error) GetCSV(context.Context, string) ([]byte, *Response, error) List(context.Context, *ListOptions) (*InvoiceList, *Response, error) GetSummary(context.Context, string) (*InvoiceSummary, *Response, error) } // InvoicesServiceOp handles communication with the Invoice related methods of // the DigitalOcean API. type InvoicesServiceOp struct { client *Client } var _ InvoicesService = &InvoicesServiceOp{} // Invoice represents a DigitalOcean Invoice type Invoice struct { InvoiceItems []InvoiceItem `json:"invoice_items"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // InvoiceItem represents a line-item on a DigitalOcean Invoice type InvoiceItem struct { Product string `json:"product"` ResourceID string `json:"resource_id"` ResourceUUID string `json:"resource_uuid"` GroupDescription string `json:"group_description"` Description string `json:"description"` Amount string `json:"amount"` Duration string `json:"duration"` DurationUnit string `json:"duration_unit"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` ProjectName string `json:"project_name"` } // InvoiceList contains a paginated list of all of a customer's invoices. // The InvoicePreview is the month-to-date usage generated by DigitalOcean. type InvoiceList struct { Invoices []InvoiceListItem `json:"invoices"` InvoicePreview InvoiceListItem `json:"invoice_preview"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // InvoiceListItem contains a small list of information about a customer's invoice. // More information can be found in the Invoice or InvoiceSummary type InvoiceListItem struct { InvoiceUUID string `json:"invoice_uuid"` Amount string `json:"amount"` InvoicePeriod string `json:"invoice_period"` UpdatedAt time.Time `json:"updated_at"` } // InvoiceSummary contains metadata and summarized usage for an invoice generated by DigitalOcean type InvoiceSummary struct { InvoiceUUID string `json:"invoice_uuid"` BillingPeriod string `json:"billing_period"` Amount string `json:"amount"` UserName string `json:"user_name"` UserBillingAddress Address `json:"user_billing_address"` UserCompany string `json:"user_company"` UserEmail string `json:"user_email"` ProductCharges InvoiceSummaryBreakdown `json:"product_charges"` Overages InvoiceSummaryBreakdown `json:"overages"` Taxes InvoiceSummaryBreakdown `json:"taxes"` CreditsAndAdjustments InvoiceSummaryBreakdown `json:"credits_and_adjustments"` } // Address represents the billing address of a customer type Address struct { AddressLine1 string `json:"address_line1"` AddressLine2 string `json:"address_line2"` City string `json:"city"` Region string `json:"region"` PostalCode string `json:"postal_code"` CountryISO2Code string `json:"country_iso2_code"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // InvoiceSummaryBreakdown is a grouped set of InvoiceItems from an invoice type InvoiceSummaryBreakdown struct { Name string `json:"name"` Amount string `json:"amount"` Items []InvoiceSummaryBreakdownItem `json:"items"` } // InvoiceSummaryBreakdownItem further breaks down the InvoiceSummary by product type InvoiceSummaryBreakdownItem struct { Name string `json:"name"` Amount string `json:"amount"` Count string `json:"count"` } func (i Invoice) String() string { return Stringify(i) } // Get detailed invoice items for an Invoice func (s *InvoicesServiceOp) Get(ctx context.Context, invoiceUUID string, opt *ListOptions) (*Invoice, *Response, error) { path := fmt.Sprintf("%s/%s", invoicesBasePath, invoiceUUID) 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(Invoice) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root, resp, err } // List invoices for a customer func (s *InvoicesServiceOp) List(ctx context.Context, opt *ListOptions) (*InvoiceList, *Response, error) { path := invoicesBasePath 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(InvoiceList) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root, resp, err } // Get a summary of metadata and summarized usage for an Invoice func (s *InvoicesServiceOp) GetSummary(ctx context.Context, invoiceUUID string) (*InvoiceSummary, *Response, error) { path := fmt.Sprintf("%s/%s/summary", invoicesBasePath, invoiceUUID) req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(InvoiceSummary) resp, err := s.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root, resp, err } // Get the pdf for an Invoice func (s *InvoicesServiceOp) GetPDF(ctx context.Context, invoiceUUID string) ([]byte, *Response, error) { path := fmt.Sprintf("%s/%s/pdf", invoicesBasePath, invoiceUUID) req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } var root bytes.Buffer resp, err := s.client.Do(ctx, req, &root) if err != nil { return nil, resp, err } return root.Bytes(), resp, err } // Get the csv for an Invoice func (s *InvoicesServiceOp) GetCSV(ctx context.Context, invoiceUUID string) ([]byte, *Response, error) { path := fmt.Sprintf("%s/%s/csv", invoicesBasePath, invoiceUUID) req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } var root bytes.Buffer resp, err := s.client.Do(ctx, req, &root) if err != nil { return nil, resp, err } return root.Bytes(), resp, err } godo-1.37.0/invoices_test.go000066400000000000000000000205331366527000000157410ustar00rootroot00000000000000package godo import ( "bytes" "fmt" "net/http" "reflect" "testing" "time" ) func TestInvoices_GetInvoices(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "invoice_items": [ { "product": "Droplets", "resource_id": "1234", "resource_uuid": "droplet-1234-uuid", "group_description": "", "description": "My Example Droplet", "amount": "12.34", "duration": "672", "duration_unit": "Hours", "start_time": "2018-06-20T08:44:38Z", "end_time": "2018-06-21T08:44:38Z", "project_name": "My project" }, { "product": "Load Balancers", "resource_id": "2345", "resource_uuid": "load-balancer-2345-uuid", "group_description": "", "description": "My Example Load Balancer", "amount": "23.45", "duration": "744", "duration_unit": "Hours", "start_time": "2018-06-20T08:44:38Z", "end_time": "2018-06-21T08:44:38Z", "project_name": "My Second Project" } ], "meta": { "total": 2 } }`) }) invoice, resp, err := client.Invoices.Get(ctx, "example-invoice-uuid", nil) if err != nil { t.Errorf("Invoices.Get returned error: %v", err) } expectedInvoiceItems := []InvoiceItem{ { Product: "Droplets", ResourceID: "1234", ResourceUUID: "droplet-1234-uuid", GroupDescription: "", Description: "My Example Droplet", Amount: "12.34", Duration: "672", DurationUnit: "Hours", StartTime: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC), EndTime: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), ProjectName: "My project", }, { Product: "Load Balancers", ResourceID: "2345", ResourceUUID: "load-balancer-2345-uuid", GroupDescription: "", Description: "My Example Load Balancer", Amount: "23.45", Duration: "744", DurationUnit: "Hours", StartTime: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC), EndTime: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), ProjectName: "My Second Project", }, } actualItems := invoice.InvoiceItems if !reflect.DeepEqual(actualItems, expectedInvoiceItems) { t.Errorf("Invoices.Get\nInvoiceItems: got=%#v\nwant=%#v", actualItems, expectedInvoiceItems) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Invoices.Get\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestInvoices_ListInvoices(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/invoices", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "invoices": [ { "invoice_uuid": "example-invoice-uuid-1", "amount": "12.34", "invoice_period": "2020-01" }, { "invoice_uuid": "example-invoice-uuid-2", "amount": "23.45", "invoice_period": "2019-12" } ], "invoice_preview": { "invoice_uuid": "example-invoice-uuid-preview", "amount": "34.56", "invoice_period": "2020-02", "updated_at": "2020-02-05T05:43:10Z" }, "meta": { "total": 2 } }`) }) invoiceListResponse, resp, err := client.Invoices.List(ctx, nil) if err != nil { t.Errorf("Invoices.List returned error: %v", err) } expectedInvoiceListItems := []InvoiceListItem{ { InvoiceUUID: "example-invoice-uuid-1", Amount: "12.34", InvoicePeriod: "2020-01", }, { InvoiceUUID: "example-invoice-uuid-2", Amount: "23.45", InvoicePeriod: "2019-12", }, } actualItems := invoiceListResponse.Invoices if !reflect.DeepEqual(actualItems, expectedInvoiceListItems) { t.Errorf("Invoices.List\nInvoiceListItems: got=%#v\nwant=%#v", actualItems, expectedInvoiceListItems) } expectedPreview := InvoiceListItem{ InvoiceUUID: "example-invoice-uuid-preview", Amount: "34.56", InvoicePeriod: "2020-02", UpdatedAt: time.Date(2020, 2, 5, 5, 43, 10, 0, time.UTC), } if !reflect.DeepEqual(invoiceListResponse.InvoicePreview, expectedPreview) { t.Errorf("Invoices.List\nInvoicePreview: got=%#v\nwant=%#v", invoiceListResponse.InvoicePreview, expectedPreview) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Invoices.List\nMeta: got=%#v\nwant=%#v", resp.Meta, expectedMeta) } } func TestInvoices_GetSummary(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid/summary", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "invoice_uuid": "example-invoice-uuid", "billing_period": "2020-01", "amount": "27.13", "user_name": "Frodo Baggins", "user_billing_address": { "address_line1": "101 Bagshot Row", "address_line2": "#2", "city": "Hobbiton", "region": "Shire", "postal_code": "12345", "country_iso2_code": "ME", "created_at": "2018-06-20T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" }, "user_company": "DigitalOcean", "user_email": "fbaggins@example.com", "product_charges": { "name": "Product usage charges", "amount": "12.34", "items": [ { "amount": "10.00", "name": "Spaces Subscription", "count": "1" }, { "amount": "2.34", "name": "Database Clusters", "count": "1" } ] }, "overages": { "name": "Overages", "amount": "3.45" }, "taxes": { "name": "Taxes", "amount": "4.56" }, "credits_and_adjustments": { "name": "Credits & adjustments", "amount": "6.78" } }`) }) invoiceSummaryResponse, _, err := client.Invoices.GetSummary(ctx, "example-invoice-uuid") if err != nil { t.Errorf("Invoices.GetSummary returned error: %v", err) } expectedSummary := InvoiceSummary{ InvoiceUUID: "example-invoice-uuid", BillingPeriod: "2020-01", Amount: "27.13", UserName: "Frodo Baggins", UserBillingAddress: Address{ AddressLine1: "101 Bagshot Row", AddressLine2: "#2", City: "Hobbiton", Region: "Shire", PostalCode: "12345", CountryISO2Code: "ME", CreatedAt: time.Date(2018, 6, 20, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, UserCompany: "DigitalOcean", UserEmail: "fbaggins@example.com", ProductCharges: InvoiceSummaryBreakdown{ Name: "Product usage charges", Amount: "12.34", Items: []InvoiceSummaryBreakdownItem{ { Name: "Spaces Subscription", Amount: "10.00", Count: "1", }, { Name: "Database Clusters", Amount: "2.34", Count: "1", }, }, }, Overages: InvoiceSummaryBreakdown{ Name: "Overages", Amount: "3.45", }, Taxes: InvoiceSummaryBreakdown{ Name: "Taxes", Amount: "4.56", }, CreditsAndAdjustments: InvoiceSummaryBreakdown{ Name: "Credits & adjustments", Amount: "6.78", }, } if !reflect.DeepEqual(invoiceSummaryResponse, &expectedSummary) { t.Errorf("Invoices.GetSummary\nInvoiceSummary: got=%#v\nwant=%#v", invoiceSummaryResponse, &expectedSummary) } } func TestInvoices_GetPDF(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid/pdf", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `some pdf content`) }) invoicePDFResponse, _, err := client.Invoices.GetPDF(ctx, "example-invoice-uuid") if err != nil { t.Errorf("Invoices.GetPDF returned error: %v", err) } expected := []byte("some pdf content") if !bytes.Equal(invoicePDFResponse, expected) { t.Errorf("Invoices.GetPDF\n got=%#v\nwant=%#v", invoicePDFResponse, expected) } } func TestInvoices_GetCSV(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/customers/my/invoices/example-invoice-uuid/csv", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `some csv content`) }) invoiceCSVResponse, _, err := client.Invoices.GetCSV(ctx, "example-invoice-uuid") if err != nil { t.Errorf("Invoices.GetCSV returned error: %v", err) } expected := []byte("some csv content") if !bytes.Equal(invoiceCSVResponse, expected) { t.Errorf("Invoices.GetCSV\n got=%#v\nwant=%#v", invoiceCSVResponse, expected) } } godo-1.37.0/keys.go000066400000000000000000000140061366527000000140340ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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) } godo-1.37.0/keys_test.go000066400000000000000000000150061366527000000150740ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestKeys_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "ssh_keys": [ { "id": 1 }, { "id": 2 } ], "meta": { "total": 2 } }`) }) keys, resp, err := client.Keys.List(ctx, nil) if err != nil { t.Errorf("Keys.List returned error: %v", err) } expectedKeys := []Key{{ID: 1}, {ID: 2}} if !reflect.DeepEqual(keys, expectedKeys) { t.Errorf("Keys.List returned keys %+v, expected %+v", keys, expectedKeys) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Keys.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestKeys_ListKeysMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/account/keys/?page=2"}}}`) }) _, resp, err := client.Keys.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestKeys_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "keys": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/account/keys/?page=3", "prev":"http://example.com/v2/account/keys/?page=1", "last":"http://example.com/v2/account/keys/?page=3", "first":"http://example.com/v2/account/keys/?page=1" } } }` mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Keys.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestKeys_GetByID(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"ssh_key": {"id":12345}}`) }) keys, _, err := client.Keys.GetByID(ctx, 12345) if err != nil { t.Errorf("Keys.GetByID returned error: %v", err) } expected := &Key{ID: 12345} if !reflect.DeepEqual(keys, expected) { t.Errorf("Keys.GetByID returned %+v, expected %+v", keys, expected) } } func TestKeys_GetByFingerprint(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"ssh_key": {"fingerprint":"aa:bb:cc"}}`) }) keys, _, err := client.Keys.GetByFingerprint(ctx, "aa:bb:cc") if err != nil { t.Errorf("Keys.GetByFingerprint returned error: %v", err) } expected := &Key{Fingerprint: "aa:bb:cc"} if !reflect.DeepEqual(keys, expected) { t.Errorf("Keys.GetByFingerprint returned %+v, expected %+v", keys, expected) } } func TestKeys_Create(t *testing.T) { setup() defer teardown() createRequest := &KeyCreateRequest{ Name: "name", PublicKey: "ssh-rsa longtextandstuff", } mux.HandleFunc("/v2/account/keys", func(w http.ResponseWriter, r *http.Request) { v := new(KeyCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) }) key, _, err := client.Keys.Create(ctx, createRequest) if err != nil { t.Errorf("Keys.Create returned error: %v", err) } expected := &Key{ID: 1} if !reflect.DeepEqual(key, expected) { t.Errorf("Keys.Create returned %+v, expected %+v", key, expected) } } func TestKeys_UpdateByID(t *testing.T) { setup() defer teardown() updateRequest := &KeyUpdateRequest{ Name: "name", } mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "name": "name", } var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body = %#v, expected %#v", v, expected) } fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) }) key, _, err := client.Keys.UpdateByID(ctx, 12345, updateRequest) if err != nil { t.Errorf("Keys.Update returned error: %v", err) } else { if id := key.ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } } } func TestKeys_UpdateByFingerprint(t *testing.T) { setup() defer teardown() updateRequest := &KeyUpdateRequest{ Name: "name", } mux.HandleFunc("/v2/account/keys/3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", func(w http.ResponseWriter, r *http.Request) { expected := map[string]interface{}{ "name": "name", } var v map[string]interface{} err := json.NewDecoder(r.Body).Decode(&v) if err != nil { t.Fatalf("decode json: %v", err) } if !reflect.DeepEqual(v, expected) { t.Errorf("Request body = %#v, expected %#v", v, expected) } fmt.Fprintf(w, `{"ssh_key":{"id":1}}`) }) key, _, err := client.Keys.UpdateByFingerprint(ctx, "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa", updateRequest) if err != nil { t.Errorf("Keys.Update returned error: %v", err) } else { if id := key.ID; id != 1 { t.Errorf("expected id '%d', received '%d'", 1, id) } } } func TestKeys_DestroyByID(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Keys.DeleteByID(ctx, 12345) if err != nil { t.Errorf("Keys.Delete returned error: %v", err) } } func TestKeys_DestroyByFingerprint(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/account/keys/aa:bb:cc", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Keys.DeleteByFingerprint(ctx, "aa:bb:cc") if err != nil { t.Errorf("Keys.Delete returned error: %v", err) } } func TestKey_String(t *testing.T) { key := &Key{ ID: 123, Name: "Key", Fingerprint: "fingerprint", PublicKey: "public key", } stringified := key.String() expected := `godo.Key{ID:123, Name:"Key", Fingerprint:"fingerprint", PublicKey:"public key"}` if expected != stringified { t.Errorf("Key.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/kubernetes.go000066400000000000000000000616351366527000000152420ustar00rootroot00000000000000package godo import ( "bytes" "context" "encoding" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) const ( kubernetesBasePath = "/v2/kubernetes" kubernetesClustersPath = kubernetesBasePath + "/clusters" kubernetesOptionsPath = kubernetesBasePath + "/options" ) // KubernetesService is an interface for interfacing with the Kubernetes endpoints // of the DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2#kubernetes type KubernetesService interface { Create(context.Context, *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error) Get(context.Context, string) (*KubernetesCluster, *Response, error) GetUser(context.Context, string) (*KubernetesClusterUser, *Response, error) GetUpgrades(context.Context, string) ([]*KubernetesVersion, *Response, error) GetKubeConfig(context.Context, string) (*KubernetesClusterConfig, *Response, error) GetCredentials(context.Context, string, *KubernetesClusterCredentialsGetRequest) (*KubernetesClusterCredentials, *Response, error) List(context.Context, *ListOptions) ([]*KubernetesCluster, *Response, error) Update(context.Context, string, *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error) Upgrade(context.Context, string, *KubernetesClusterUpgradeRequest) (*Response, error) Delete(context.Context, string) (*Response, error) CreateNodePool(ctx context.Context, clusterID string, req *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error) GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error) ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error) UpdateNodePool(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error) // RecycleNodePoolNodes is DEPRECATED please use DeleteNode // The method will be removed in godo 2.0. RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolRecycleNodesRequest) (*Response, error) DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error) DeleteNode(ctx context.Context, clusterID, poolID, nodeID string, req *KubernetesNodeDeleteRequest) (*Response, error) GetOptions(context.Context) (*KubernetesOptions, *Response, error) } var _ KubernetesService = &KubernetesServiceOp{} // KubernetesServiceOp handles communication with Kubernetes methods of the DigitalOcean API. type KubernetesServiceOp struct { client *Client } // KubernetesClusterCreateRequest represents a request to create a Kubernetes cluster. type KubernetesClusterCreateRequest struct { Name string `json:"name,omitempty"` RegionSlug string `json:"region,omitempty"` VersionSlug string `json:"version,omitempty"` Tags []string `json:"tags,omitempty"` VPCUUID string `json:"vpc_uuid,omitempty"` NodePools []*KubernetesNodePoolCreateRequest `json:"node_pools,omitempty"` MaintenancePolicy *KubernetesMaintenancePolicy `json:"maintenance_policy"` AutoUpgrade bool `json:"auto_upgrade"` } // KubernetesClusterUpdateRequest represents a request to update a Kubernetes cluster. type KubernetesClusterUpdateRequest struct { Name string `json:"name,omitempty"` Tags []string `json:"tags,omitempty"` MaintenancePolicy *KubernetesMaintenancePolicy `json:"maintenance_policy,omitempty"` AutoUpgrade *bool `json:"auto_upgrade,omitempty"` } // KubernetesClusterUpgradeRequest represents a request to upgrade a Kubernetes cluster. type KubernetesClusterUpgradeRequest struct { VersionSlug string `json:"version,omitempty"` } // KubernetesNodePoolCreateRequest represents a request to create a node pool for a // Kubernetes cluster. type KubernetesNodePoolCreateRequest struct { Name string `json:"name,omitempty"` Size string `json:"size,omitempty"` Count int `json:"count,omitempty"` Tags []string `json:"tags,omitempty"` Labels map[string]string `json:"labels,omitempty"` AutoScale bool `json:"auto_scale,omitempty"` MinNodes int `json:"min_nodes,omitempty"` MaxNodes int `json:"max_nodes,omitempty"` } // KubernetesNodePoolUpdateRequest represents a request to update a node pool in a // Kubernetes cluster. type KubernetesNodePoolUpdateRequest struct { Name string `json:"name,omitempty"` Count *int `json:"count,omitempty"` Tags []string `json:"tags,omitempty"` Labels map[string]string `json:"labels,omitempty"` AutoScale *bool `json:"auto_scale,omitempty"` MinNodes *int `json:"min_nodes,omitempty"` MaxNodes *int `json:"max_nodes,omitempty"` } // KubernetesNodePoolRecycleNodesRequest is DEPRECATED please use DeleteNode // The type will be removed in godo 2.0. type KubernetesNodePoolRecycleNodesRequest struct { Nodes []string `json:"nodes,omitempty"` } // KubernetesNodeDeleteRequest is a request to delete a specific node in a node pool. type KubernetesNodeDeleteRequest struct { // Replace will cause a new node to be created to replace the deleted node. Replace bool `json:"replace,omitempty"` // SkipDrain skips draining the node before deleting it. SkipDrain bool `json:"skip_drain,omitempty"` } // KubernetesClusterCredentialsGetRequest is a request to get cluster credentials. type KubernetesClusterCredentialsGetRequest struct { ExpirySeconds *int `json:"expiry_seconds,omitempty"` } // KubernetesCluster represents a Kubernetes cluster. type KubernetesCluster struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` RegionSlug string `json:"region,omitempty"` VersionSlug string `json:"version,omitempty"` ClusterSubnet string `json:"cluster_subnet,omitempty"` ServiceSubnet string `json:"service_subnet,omitempty"` IPv4 string `json:"ipv4,omitempty"` Endpoint string `json:"endpoint,omitempty"` Tags []string `json:"tags,omitempty"` VPCUUID string `json:"vpc_uuid,omitempty"` NodePools []*KubernetesNodePool `json:"node_pools,omitempty"` MaintenancePolicy *KubernetesMaintenancePolicy `json:"maintenance_policy,omitempty"` AutoUpgrade bool `json:"auto_upgrade,omitempty"` Status *KubernetesClusterStatus `json:"status,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } // KubernetesClusterUser represents a Kubernetes cluster user. type KubernetesClusterUser struct { Username string `json:"username,omitempty"` Groups []string `json:"groups,omitempty"` } // KubernetesClusterCredentials represents Kubernetes cluster credentials. type KubernetesClusterCredentials struct { Server string `json:"server"` CertificateAuthorityData []byte `json:"certificate_authority_data"` ClientCertificateData []byte `json:"client_certificate_data"` ClientKeyData []byte `json:"client_key_data"` Token string `json:"token"` ExpiresAt time.Time `json:"expires_at"` } // KubernetesMaintenancePolicy is a configuration to set the maintenance window // of a cluster type KubernetesMaintenancePolicy struct { StartTime string `json:"start_time"` Duration string `json:"duration"` Day KubernetesMaintenancePolicyDay `json:"day"` } // KubernetesMaintenancePolicyDay represents the possible days of a maintenance // window type KubernetesMaintenancePolicyDay int const ( KubernetesMaintenanceDayAny KubernetesMaintenancePolicyDay = iota KubernetesMaintenanceDayMonday KubernetesMaintenanceDayTuesday KubernetesMaintenanceDayWednesday KubernetesMaintenanceDayThursday KubernetesMaintenanceDayFriday KubernetesMaintenanceDaySaturday KubernetesMaintenanceDaySunday ) var ( days = [...]string{ "any", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday", } toDay = map[string]KubernetesMaintenancePolicyDay{ "any": KubernetesMaintenanceDayAny, "monday": KubernetesMaintenanceDayMonday, "tuesday": KubernetesMaintenanceDayTuesday, "wednesday": KubernetesMaintenanceDayWednesday, "thursday": KubernetesMaintenanceDayThursday, "friday": KubernetesMaintenanceDayFriday, "saturday": KubernetesMaintenanceDaySaturday, "sunday": KubernetesMaintenanceDaySunday, } ) // KubernetesMaintenanceToDay returns the appropriate KubernetesMaintenancePolicyDay for the given string. func KubernetesMaintenanceToDay(day string) (KubernetesMaintenancePolicyDay, error) { d, ok := toDay[day] if !ok { return 0, fmt.Errorf("unknown day: %q", day) } return d, nil } func (k KubernetesMaintenancePolicyDay) String() string { if KubernetesMaintenanceDayAny <= k && k <= KubernetesMaintenanceDaySunday { return days[k] } return fmt.Sprintf("%d !Weekday", k) } func (k *KubernetesMaintenancePolicyDay) UnmarshalJSON(data []byte) error { var val string if err := json.Unmarshal(data, &val); err != nil { return err } parsed, err := KubernetesMaintenanceToDay(val) if err != nil { return err } *k = parsed return nil } func (k KubernetesMaintenancePolicyDay) MarshalJSON() ([]byte, error) { if KubernetesMaintenanceDayAny <= k && k <= KubernetesMaintenanceDaySunday { return json.Marshal(days[k]) } return nil, fmt.Errorf("invalid day: %d", k) } // Possible states for a cluster. const ( KubernetesClusterStatusProvisioning = KubernetesClusterStatusState("provisioning") KubernetesClusterStatusRunning = KubernetesClusterStatusState("running") KubernetesClusterStatusDegraded = KubernetesClusterStatusState("degraded") KubernetesClusterStatusError = KubernetesClusterStatusState("error") KubernetesClusterStatusDeleted = KubernetesClusterStatusState("deleted") KubernetesClusterStatusUpgrading = KubernetesClusterStatusState("upgrading") KubernetesClusterStatusInvalid = KubernetesClusterStatusState("invalid") ) // KubernetesClusterStatusState represents states for a cluster. type KubernetesClusterStatusState string var _ encoding.TextUnmarshaler = (*KubernetesClusterStatusState)(nil) // UnmarshalText unmarshals the state. func (s *KubernetesClusterStatusState) UnmarshalText(text []byte) error { switch KubernetesClusterStatusState(strings.ToLower(string(text))) { case KubernetesClusterStatusProvisioning: *s = KubernetesClusterStatusProvisioning case KubernetesClusterStatusRunning: *s = KubernetesClusterStatusRunning case KubernetesClusterStatusDegraded: *s = KubernetesClusterStatusDegraded case KubernetesClusterStatusError: *s = KubernetesClusterStatusError case KubernetesClusterStatusDeleted: *s = KubernetesClusterStatusDeleted case KubernetesClusterStatusUpgrading: *s = KubernetesClusterStatusUpgrading case "", KubernetesClusterStatusInvalid: *s = KubernetesClusterStatusInvalid default: return fmt.Errorf("unknown cluster state %q", string(text)) } return nil } // KubernetesClusterStatus describes the status of a cluster. type KubernetesClusterStatus struct { State KubernetesClusterStatusState `json:"state,omitempty"` Message string `json:"message,omitempty"` } // KubernetesNodePool represents a node pool in a Kubernetes cluster. type KubernetesNodePool struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Size string `json:"size,omitempty"` Count int `json:"count,omitempty"` Tags []string `json:"tags,omitempty"` Labels map[string]string `json:"labels,omitempty"` AutoScale bool `json:"auto_scale,omitempty"` MinNodes int `json:"min_nodes,omitempty"` MaxNodes int `json:"max_nodes,omitempty"` Nodes []*KubernetesNode `json:"nodes,omitempty"` } // KubernetesNode represents a Node in a node pool in a Kubernetes cluster. type KubernetesNode struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` Status *KubernetesNodeStatus `json:"status,omitempty"` DropletID string `json:"droplet_id,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } // KubernetesNodeStatus represents the status of a particular Node in a Kubernetes cluster. type KubernetesNodeStatus struct { State string `json:"state,omitempty"` Message string `json:"message,omitempty"` } // KubernetesOptions represents options available for creating Kubernetes clusters. type KubernetesOptions struct { Versions []*KubernetesVersion `json:"versions,omitempty"` Regions []*KubernetesRegion `json:"regions,omitempty"` Sizes []*KubernetesNodeSize `json:"sizes,omitempty"` } // KubernetesVersion is a DigitalOcean Kubernetes release. type KubernetesVersion struct { Slug string `json:"slug,omitempty"` KubernetesVersion string `json:"kubernetes_version,omitempty"` } // KubernetesNodeSize is a node sizes supported for Kubernetes clusters. type KubernetesNodeSize struct { Name string `json:"name"` Slug string `json:"slug"` } // KubernetesRegion is a region usable by Kubernetes clusters. type KubernetesRegion struct { Name string `json:"name"` Slug string `json:"slug"` } type kubernetesClustersRoot struct { Clusters []*KubernetesCluster `json:"kubernetes_clusters,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } type kubernetesClusterRoot struct { Cluster *KubernetesCluster `json:"kubernetes_cluster,omitempty"` } type kubernetesClusterUserRoot struct { User *KubernetesClusterUser `json:"kubernetes_cluster_user,omitempty"` } type kubernetesNodePoolRoot struct { NodePool *KubernetesNodePool `json:"node_pool,omitempty"` } type kubernetesNodePoolsRoot struct { NodePools []*KubernetesNodePool `json:"node_pools,omitempty"` Links *Links `json:"links,omitempty"` } type kubernetesUpgradesRoot struct { AvailableUpgradeVersions []*KubernetesVersion `json:"available_upgrade_versions,omitempty"` } // Get retrieves the details of a Kubernetes cluster. func (svc *KubernetesServiceOp) Get(ctx context.Context, clusterID string) (*KubernetesCluster, *Response, error) { path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kubernetesClusterRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Cluster, resp, nil } // GetUser retrieves the details of a Kubernetes cluster user. func (svc *KubernetesServiceOp) GetUser(ctx context.Context, clusterID string) (*KubernetesClusterUser, *Response, error) { path := fmt.Sprintf("%s/%s/user", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kubernetesClusterUserRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.User, resp, nil } // GetUpgrades retrieves versions a Kubernetes cluster can be upgraded to. An // upgrade can be requested using `Upgrade`. func (svc *KubernetesServiceOp) GetUpgrades(ctx context.Context, clusterID string) ([]*KubernetesVersion, *Response, error) { path := fmt.Sprintf("%s/%s/upgrades", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kubernetesUpgradesRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, nil, err } return root.AvailableUpgradeVersions, resp, nil } // Create creates a Kubernetes cluster. func (svc *KubernetesServiceOp) Create(ctx context.Context, create *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error) { path := kubernetesClustersPath req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create) if err != nil { return nil, nil, err } root := new(kubernetesClusterRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Cluster, resp, nil } // Delete deletes a Kubernetes cluster. There is no way to recover a cluster // once it has been destroyed. func (svc *KubernetesServiceOp) Delete(ctx context.Context, clusterID string) (*Response, error) { path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // List returns a list of the Kubernetes clusters visible with the caller's API token. func (svc *KubernetesServiceOp) List(ctx context.Context, opts *ListOptions) ([]*KubernetesCluster, *Response, error) { path := kubernetesClustersPath path, err := addOptions(path, opts) 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(kubernetesClustersRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Clusters, resp, nil } // KubernetesClusterConfig is the content of a Kubernetes config file, which can be // used to interact with your Kubernetes cluster using `kubectl`. // See: https://kubernetes.io/docs/tasks/tools/install-kubectl/ type KubernetesClusterConfig struct { KubeconfigYAML []byte } // GetKubeConfig returns a Kubernetes config file for the specified cluster. func (svc *KubernetesServiceOp) GetKubeConfig(ctx context.Context, clusterID string) (*KubernetesClusterConfig, *Response, error) { path := fmt.Sprintf("%s/%s/kubeconfig", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } configBytes := bytes.NewBuffer(nil) resp, err := svc.client.Do(ctx, req, configBytes) if err != nil { return nil, resp, err } res := &KubernetesClusterConfig{ KubeconfigYAML: configBytes.Bytes(), } return res, resp, nil } // GetCredentials returns a Kubernetes API server credentials for the specified cluster. func (svc *KubernetesServiceOp) GetCredentials(ctx context.Context, clusterID string, get *KubernetesClusterCredentialsGetRequest) (*KubernetesClusterCredentials, *Response, error) { path := fmt.Sprintf("%s/%s/credentials", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } q := req.URL.Query() if get.ExpirySeconds != nil { q.Add("expiry_seconds", strconv.Itoa(*get.ExpirySeconds)) } req.URL.RawQuery = q.Encode() credentials := new(KubernetesClusterCredentials) resp, err := svc.client.Do(ctx, req, credentials) if err != nil { return nil, nil, err } return credentials, resp, nil } // Update updates a Kubernetes cluster's properties. func (svc *KubernetesServiceOp) Update(ctx context.Context, clusterID string, update *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error) { path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update) if err != nil { return nil, nil, err } root := new(kubernetesClusterRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Cluster, resp, nil } // Upgrade upgrades a Kubernetes cluster to a new version. Valid upgrade // versions for a given cluster can be retrieved with `GetUpgrades`. func (svc *KubernetesServiceOp) Upgrade(ctx context.Context, clusterID string, upgrade *KubernetesClusterUpgradeRequest) (*Response, error) { path := fmt.Sprintf("%s/%s/upgrade", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, upgrade) if err != nil { return nil, err } return svc.client.Do(ctx, req, nil) } // CreateNodePool creates a new node pool in an existing Kubernetes cluster. func (svc *KubernetesServiceOp) CreateNodePool(ctx context.Context, clusterID string, create *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error) { path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create) if err != nil { return nil, nil, err } root := new(kubernetesNodePoolRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.NodePool, resp, nil } // GetNodePool retrieves an existing node pool in a Kubernetes cluster. func (svc *KubernetesServiceOp) GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error) { path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kubernetesNodePoolRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.NodePool, resp, nil } // ListNodePools lists all the node pools found in a Kubernetes cluster. func (svc *KubernetesServiceOp) ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error) { path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID) path, err := addOptions(path, opts) 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(kubernetesNodePoolsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.NodePools, resp, nil } // UpdateNodePool updates the details of an existing node pool. func (svc *KubernetesServiceOp) UpdateNodePool(ctx context.Context, clusterID, poolID string, update *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error) { path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update) if err != nil { return nil, nil, err } root := new(kubernetesNodePoolRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.NodePool, resp, nil } // RecycleNodePoolNodes is DEPRECATED please use DeleteNode // The method will be removed in godo 2.0. func (svc *KubernetesServiceOp) RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, recycle *KubernetesNodePoolRecycleNodesRequest) (*Response, error) { path := fmt.Sprintf("%s/%s/node_pools/%s/recycle", kubernetesClustersPath, clusterID, poolID) req, err := svc.client.NewRequest(ctx, http.MethodPost, path, recycle) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // DeleteNodePool deletes a node pool, and subsequently all the nodes in that pool. func (svc *KubernetesServiceOp) DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error) { path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // DeleteNode deletes a specific node in a node pool. func (svc *KubernetesServiceOp) DeleteNode(ctx context.Context, clusterID, poolID, nodeID string, deleteReq *KubernetesNodeDeleteRequest) (*Response, error) { path := fmt.Sprintf("%s/%s/node_pools/%s/nodes/%s", kubernetesClustersPath, clusterID, poolID, nodeID) if deleteReq != nil { v := make(url.Values) if deleteReq.SkipDrain { v.Set("skip_drain", "1") } if deleteReq.Replace { v.Set("replace", "1") } if query := v.Encode(); query != "" { path = path + "?" + query } } req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } type kubernetesOptionsRoot struct { Options *KubernetesOptions `json:"options,omitempty"` Links *Links `json:"links,omitempty"` } // GetOptions returns options about the Kubernetes service, such as the versions available for // cluster creation. func (svc *KubernetesServiceOp) GetOptions(ctx context.Context) (*KubernetesOptions, *Response, error) { path := kubernetesOptionsPath req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(kubernetesOptionsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Options, resp, nil } godo-1.37.0/kubernetes_test.go000066400000000000000000001147461366527000000163030ustar00rootroot00000000000000package godo import ( "bytes" "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestKubernetesClusters_ListClusters(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes wantClusters := []*KubernetesCluster{ &KubernetesCluster{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "blablabla", RegionSlug: "nyc1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", IPv4: "", Tags: []string(nil), VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", Status: &KubernetesClusterStatus{ State: KubernetesClusterStatusRunning, }, NodePools: []*KubernetesNodePool{ { ID: "1a17a012-cb31-4886-a787-deadbeef1191", Name: "blablabla-1", Size: "s-1vcpu-2gb", Count: 2, Nodes: []*KubernetesNode{ { ID: "", Name: "", Status: &KubernetesNodeStatus{}, DropletID: "droplet-1", CreatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, { ID: "", Name: "", Status: &KubernetesNodeStatus{}, DropletID: "droplet-2", CreatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, }, }, }, CreatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, &KubernetesCluster{ ID: "deadbeef-dead-4aa5-beef-deadbeef347d", Name: "antoine", RegionSlug: "nyc1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", IPv4: "1.2.3.4", VPCUUID: "880b7f98-f062-404d-b33c-458d545696f7", Status: &KubernetesClusterStatus{ State: KubernetesClusterStatusRunning, }, NodePools: []*KubernetesNodePool{ { ID: "deadbeef-dead-beef-dead-deadbeefb4b3", Name: "antoine-1", Size: "s-1vcpu-2gb", Count: 5, Labels: map[string]string{ "foo": "bar", }, Nodes: []*KubernetesNode{ { ID: "deadbeef-dead-beef-dead-deadbeefb4b1", Name: "worker-393", Status: &KubernetesNodeStatus{State: "running"}, DropletID: "droplet-3", CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), }, { ID: "deadbeef-dead-beef-dead-deadbeefb4b2", Name: "worker-394", Status: &KubernetesNodeStatus{State: "running"}, DropletID: "droplet-4", CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), }, }, }, }, CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), }, } jBlob := ` { "kubernetes_clusters": [ { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "blablabla", "region": "nyc1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "ipv4": "", "tags": null, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "status": { "state": "running" }, "node_pools": [ { "id": "1a17a012-cb31-4886-a787-deadbeef1191", "name": "blablabla-1", "version": "1.10.0-gen0", "size": "s-1vcpu-2gb", "count": 2, "tags": null, "labels": null, "nodes": [ { "id": "", "name": "", "status": { "state": "" }, "droplet_id": "droplet-1", "created_at": "2018-06-21T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" }, { "id": "", "name": "", "status": { "state": "" }, "droplet_id": "droplet-2", "created_at": "2018-06-21T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" } ] } ], "created_at": "2018-06-21T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" }, { "id": "deadbeef-dead-4aa5-beef-deadbeef347d", "name": "antoine", "region": "nyc1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "ipv4": "1.2.3.4", "tags": null, "status": { "state": "running" }, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f7", "node_pools": [ { "id": "deadbeef-dead-beef-dead-deadbeefb4b3", "name": "antoine-1", "version": "1.10.0-gen0", "size": "s-1vcpu-2gb", "count": 5, "tags": null, "labels": { "foo": "bar" }, "nodes": [ { "id": "deadbeef-dead-beef-dead-deadbeefb4b1", "name": "worker-393", "status": { "state": "running" }, "droplet_id": "droplet-3", "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" }, { "id": "deadbeef-dead-beef-dead-deadbeefb4b2", "name": "worker-394", "status": { "state": "running" }, "droplet_id": "droplet-4", "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" } ] } ], "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" } ], "links": { "pages": { "next": "https://api.digitalocean.com/v2/kubernetes/clusters?page=2", "last": "https://api.digitalocean.com/v2/kubernetes/clusters?page=2" } }, "meta": { "total": 2 } }` mux.HandleFunc("/v2/kubernetes/clusters", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) gotClusters, resp, err := kubeSvc.List(ctx, nil) require.NoError(t, err) assert.Equal(t, wantClusters, gotClusters) gotRespLinks := resp.Links wantRespLinks := &Links{ Pages: &Pages{ Next: "https://api.digitalocean.com/v2/kubernetes/clusters?page=2", Last: "https://api.digitalocean.com/v2/kubernetes/clusters?page=2", }, } assert.Equal(t, wantRespLinks, gotRespLinks) gotRespMeta := resp.Meta wantRespMeta := &Meta{ Total: 2, } assert.Equal(t, wantRespMeta, gotRespMeta) } func TestKubernetesClusters_Get(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesCluster{ ID: "deadbeef-dead-4aa5-beef-deadbeef347d", Name: "antoine", RegionSlug: "nyc1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", IPv4: "1.2.3.4", VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", Status: &KubernetesClusterStatus{ State: KubernetesClusterStatusRunning, }, NodePools: []*KubernetesNodePool{ { ID: "deadbeef-dead-beef-dead-deadbeefb4b3", Name: "antoine-1", Size: "s-1vcpu-2gb", Count: 5, Labels: map[string]string{ "foo": "bar", }, Nodes: []*KubernetesNode{ { ID: "deadbeef-dead-beef-dead-deadbeefb4b1", Name: "worker-393", Status: &KubernetesNodeStatus{State: "running"}, DropletID: "droplet-1", CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), }, { ID: "deadbeef-dead-beef-dead-deadbeefb4b2", Name: "worker-394", Status: &KubernetesNodeStatus{State: "running"}, DropletID: "droplet-2", CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), }, }, }, }, MaintenancePolicy: &KubernetesMaintenancePolicy{ StartTime: "00:00", Day: KubernetesMaintenanceDayMonday, }, CreatedAt: time.Date(2018, 6, 15, 7, 10, 23, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 15, 7, 11, 26, 0, time.UTC), } jBlob := ` { "kubernetes_cluster": { "id": "deadbeef-dead-4aa5-beef-deadbeef347d", "name": "antoine", "region": "nyc1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "ipv4": "1.2.3.4", "tags": null, "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "status": { "state": "running" }, "node_pools": [ { "id": "deadbeef-dead-beef-dead-deadbeefb4b3", "name": "antoine-1", "version": "1.10.0-gen0", "size": "s-1vcpu-2gb", "count": 5, "tags": null, "labels": { "foo": "bar" }, "nodes": [ { "id": "deadbeef-dead-beef-dead-deadbeefb4b1", "name": "worker-393", "status": { "state": "running" }, "droplet_id": "droplet-1", "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" }, { "id": "deadbeef-dead-beef-dead-deadbeefb4b2", "name": "worker-394", "status": { "state": "running" }, "droplet_id": "droplet-2", "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" } ] } ], "maintenance_policy": { "start_time": "00:00", "day": "monday" }, "created_at": "2018-06-15T07:10:23Z", "updated_at": "2018-06-15T07:11:26Z" } }` mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.Get(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_GetUser(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesClusterUser{ Username: "foo@example.com", Groups: []string{ "foo:bar", }, } jBlob := ` { "kubernetes_cluster_user": { "username": "foo@example.com", "groups": ["foo:bar"] } }` mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/user", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetUser(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_GetKubeConfig(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := "some YAML" blob := []byte(want) mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/kubeconfig", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, want) }) got, _, err := kubeSvc.GetKubeConfig(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) require.Equal(t, blob, got.KubeconfigYAML) } func TestKubernetesClusters_GetCredentials(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes timestamp, err := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z") require.NoError(t, err) want := &KubernetesClusterCredentials{ Token: "secret", ExpiresAt: timestamp, } jBlob := ` { "token": "secret", "expires_at": "2014-11-12T11:45:26.371Z" }` mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/credentials", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) assert.Empty(t, r.URL.Query()) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetCredentials(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", &KubernetesClusterCredentialsGetRequest{}) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_GetCredentials_WithExpirySeconds(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes timestamp, err := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z") require.NoError(t, err) want := &KubernetesClusterCredentials{ Token: "secret", ExpiresAt: timestamp, } jBlob := ` { "token": "secret", "expires_at": "2014-11-12T11:45:26.371Z" }` mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/credentials", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expirySeconds, ok := r.URL.Query()["expiry_seconds"] assert.True(t, ok) assert.Len(t, expirySeconds, 1) assert.Contains(t, expirySeconds, "3600") fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetCredentials(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", &KubernetesClusterCredentialsGetRequest{ ExpirySeconds: intPtr(60 * 60), }) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_GetUpgrades(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := []*KubernetesVersion{ { Slug: "1.12.3-do.2", KubernetesVersion: "1.12.3", }, { Slug: "1.13.1-do.1", KubernetesVersion: "1.13.1", }, } jBlob := ` { "available_upgrade_versions": [ { "slug": "1.12.3-do.2", "kubernetes_version": "1.12.3" }, { "slug": "1.13.1-do.1", "kubernetes_version": "1.13.1" } ] } ` mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/upgrades", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetUpgrades(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_Create(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesCluster{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "antoine-test-cluster", RegionSlug: "s2r1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", Tags: []string{"cluster-tag-1", "cluster-tag-2"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", NodePools: []*KubernetesNodePool{ &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, Labels: map[string]string{"foo": "bar"}, }, }, MaintenancePolicy: &KubernetesMaintenancePolicy{ StartTime: "00:00", Day: KubernetesMaintenanceDayMonday, }, } createRequest := &KubernetesClusterCreateRequest{ Name: want.Name, RegionSlug: want.RegionSlug, VersionSlug: want.VersionSlug, Tags: want.Tags, VPCUUID: want.VPCUUID, NodePools: []*KubernetesNodePoolCreateRequest{ &KubernetesNodePoolCreateRequest{ Size: want.NodePools[0].Size, Count: want.NodePools[0].Count, Name: want.NodePools[0].Name, Tags: want.NodePools[0].Tags, Labels: want.NodePools[0].Labels, AutoScale: want.NodePools[0].AutoScale, MinNodes: want.NodePools[0].MinNodes, MaxNodes: want.NodePools[0].MaxNodes, }, }, MaintenancePolicy: want.MaintenancePolicy, } jBlob := ` { "kubernetes_cluster": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "antoine-test-cluster", "region": "s2r1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "tags": [ "cluster-tag-1", "cluster-tag-2" ], "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "node_pools": [ { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ], "labels": { "foo": "bar" } } ], "maintenance_policy": { "start_time": "00:00", "day": "monday" } } }` mux.HandleFunc("/v2/kubernetes/clusters", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesClusterCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.Create(ctx, createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_Create_AutoScalePool(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesCluster{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "antoine-test-cluster", RegionSlug: "s2r1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", Tags: []string{"cluster-tag-1", "cluster-tag-2"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", NodePools: []*KubernetesNodePool{ &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, AutoScale: true, MinNodes: 0, MaxNodes: 10, }, }, MaintenancePolicy: &KubernetesMaintenancePolicy{ StartTime: "00:00", Day: KubernetesMaintenanceDayMonday, }, } createRequest := &KubernetesClusterCreateRequest{ Name: want.Name, RegionSlug: want.RegionSlug, VersionSlug: want.VersionSlug, Tags: want.Tags, VPCUUID: want.VPCUUID, NodePools: []*KubernetesNodePoolCreateRequest{ &KubernetesNodePoolCreateRequest{ Size: want.NodePools[0].Size, Count: want.NodePools[0].Count, Name: want.NodePools[0].Name, Tags: want.NodePools[0].Tags, AutoScale: want.NodePools[0].AutoScale, MinNodes: want.NodePools[0].MinNodes, MaxNodes: want.NodePools[0].MaxNodes, }, }, MaintenancePolicy: want.MaintenancePolicy, } jBlob := ` { "kubernetes_cluster": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "antoine-test-cluster", "region": "s2r1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "tags": [ "cluster-tag-1", "cluster-tag-2" ], "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "node_pools": [ { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ], "auto_scale": true, "min_nodes": 0, "max_nodes": 10 } ], "maintenance_policy": { "start_time": "00:00", "day": "monday" } } }` mux.HandleFunc("/v2/kubernetes/clusters", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesClusterCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.Create(ctx, createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_Update(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesCluster{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "antoine-test-cluster", RegionSlug: "s2r1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", Tags: []string{"cluster-tag-1", "cluster-tag-2"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", NodePools: []*KubernetesNodePool{ &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, Labels: map[string]string{ "foo": "bar", }, }, }, MaintenancePolicy: &KubernetesMaintenancePolicy{ StartTime: "00:00", Day: KubernetesMaintenanceDayMonday, }, } updateRequest := &KubernetesClusterUpdateRequest{ Name: want.Name, Tags: want.Tags, MaintenancePolicy: want.MaintenancePolicy, } jBlob := ` { "kubernetes_cluster": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "antoine-test-cluster", "region": "s2r1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "tags": [ "cluster-tag-1", "cluster-tag-2" ], "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "node_pools": [ { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ], "labels": { "foo": "bar" } } ], "maintenance_policy": { "start_time": "00:00", "day": "monday" } } }` expectedReqJSON := `{"name":"antoine-test-cluster","tags":["cluster-tag-1","cluster-tag-2"],"maintenance_policy":{"start_time":"00:00","duration":"","day":"monday"}} ` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f", func(w http.ResponseWriter, r *http.Request) { buf := new(bytes.Buffer) buf.ReadFrom(r.Body) require.Equal(t, expectedReqJSON, buf.String()) v := new(KubernetesClusterUpdateRequest) err := json.NewDecoder(buf).Decode(v) require.NoError(t, err) testMethod(t, r, http.MethodPut) require.Equal(t, v, updateRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.Update(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", updateRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_Update_FalseAutoUpgrade(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesCluster{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8f", Name: "antoine-test-cluster", RegionSlug: "s2r1", VersionSlug: "1.10.0-gen0", ClusterSubnet: "10.244.0.0/16", ServiceSubnet: "10.245.0.0/16", Tags: []string{"cluster-tag-1", "cluster-tag-2"}, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", NodePools: []*KubernetesNodePool{ &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, }, }, MaintenancePolicy: &KubernetesMaintenancePolicy{ StartTime: "00:00", Day: KubernetesMaintenanceDayMonday, }, } updateRequest := &KubernetesClusterUpdateRequest{ AutoUpgrade: boolPtr(false), } jBlob := ` { "kubernetes_cluster": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "name": "antoine-test-cluster", "region": "s2r1", "version": "1.10.0-gen0", "cluster_subnet": "10.244.0.0/16", "service_subnet": "10.245.0.0/16", "tags": [ "cluster-tag-1", "cluster-tag-2" ], "vpc_uuid": "880b7f98-f062-404d-b33c-458d545696f6", "node_pools": [ { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ] } ], "maintenance_policy": { "start_time": "00:00", "day": "monday" } } }` expectedReqJSON := `{"auto_upgrade":false} ` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f", func(w http.ResponseWriter, r *http.Request) { buf := new(bytes.Buffer) buf.ReadFrom(r.Body) require.Equal(t, expectedReqJSON, buf.String()) v := new(KubernetesClusterUpdateRequest) err := json.NewDecoder(buf).Decode(v) require.NoError(t, err) testMethod(t, r, http.MethodPut) require.Equal(t, v, updateRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.Update(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", updateRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_Upgrade(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes upgradeRequest := &KubernetesClusterUpgradeRequest{ VersionSlug: "1.12.3-do.2", } mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/upgrade", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesClusterUpgradeRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, upgradeRequest) }) _, err := kubeSvc.Upgrade(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", upgradeRequest) require.NoError(t, err) } func TestKubernetesClusters_Destroy(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := kubeSvc.Delete(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d") require.NoError(t, err) } func TestKubernetesClusters_CreateNodePool(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, Labels: map[string]string{"foo": "bar"}, AutoScale: false, MinNodes: 0, MaxNodes: 0, } createRequest := &KubernetesNodePoolCreateRequest{ Size: want.Size, Count: want.Count, Name: want.Name, Tags: want.Tags, } jBlob := ` { "node_pool": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ], "labels": { "foo": "bar" } } }` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesNodePoolCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.CreateNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_CreateNodePool_AutoScale(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, AutoScale: true, MinNodes: 0, MaxNodes: 10, } createRequest := &KubernetesNodePoolCreateRequest{ Size: want.Size, Count: want.Count, Name: want.Name, Tags: want.Tags, AutoScale: want.AutoScale, MinNodes: want.MinNodes, MaxNodes: want.MaxNodes, } jBlob := ` { "node_pool": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ], "auto_scale": true, "min_nodes": 0, "max_nodes": 10 } }` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesNodePoolCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.CreateNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_GetNodePool(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-0739-4a1a-acc5-deadbeefbb8a", Size: "s-1vcpu-1gb", Count: 2, Name: "pool-a", Tags: []string{"tag-1"}, } jBlob := ` { "node_pool": { "id": "8d91899c-0739-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 2, "name": "pool-a", "tags": [ "tag-1" ] } }` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools/8d91899c-0739-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "8d91899c-0739-4a1a-acc5-deadbeefbb8a") require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_ListNodePools(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := []*KubernetesNodePool{ { ID: "1a17a012-cb31-4886-a787-deadbeef1191", Name: "blablabla-1", Size: "s-1vcpu-2gb", Count: 2, Nodes: []*KubernetesNode{ { ID: "", Name: "", Status: &KubernetesNodeStatus{}, CreatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, { ID: "", Name: "", Status: &KubernetesNodeStatus{}, CreatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), UpdatedAt: time.Date(2018, 6, 21, 8, 44, 38, 0, time.UTC), }, }, }, } jBlob := ` { "node_pools": [ { "id": "1a17a012-cb31-4886-a787-deadbeef1191", "name": "blablabla-1", "version": "1.10.0-gen0", "size": "s-1vcpu-2gb", "count": 2, "tags": null, "nodes": [ { "id": "", "name": "", "status": { "state": "" }, "created_at": "2018-06-21T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" }, { "id": "", "name": "", "status": { "state": "" }, "created_at": "2018-06-21T08:44:38Z", "updated_at": "2018-06-21T08:44:38Z" } ] } ] }` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.ListNodePools(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", nil) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_UpdateNodePool(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", Name: "a better name", Size: "s-1vcpu-1gb", Count: 4, Tags: []string{"tag-1", "tag-2"}, Labels: map[string]string{"foo": "bar"}, AutoScale: false, MinNodes: 0, MaxNodes: 0, } updateRequest := &KubernetesNodePoolUpdateRequest{ Name: "a better name", Count: intPtr(4), Tags: []string{"tag-1", "tag-2"}, } jBlob := ` { "node_pool": { "id": "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 4, "name": "a better name", "tags": [ "tag-1", "tag-2" ], "labels": { "foo": "bar" } } }` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesNodePoolUpdateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPut) require.Equal(t, v, updateRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.UpdateNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", updateRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_UpdateNodePool_ZeroCount(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", Name: "name", Size: "s-1vcpu-1gb", Count: 0, Tags: []string{"tag-1", "tag-2"}, AutoScale: false, MinNodes: 0, MaxNodes: 0, } updateRequest := &KubernetesNodePoolUpdateRequest{ Count: intPtr(0), } jBlob := ` { "node_pool": { "id": "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 0, "name": "name", "tags": [ "tag-1", "tag-2" ] } }` expectedReqJSON := `{"count":0} ` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { buf := new(bytes.Buffer) buf.ReadFrom(r.Body) require.Equal(t, expectedReqJSON, buf.String()) v := new(KubernetesNodePoolUpdateRequest) err := json.NewDecoder(buf).Decode(v) require.NoError(t, err) testMethod(t, r, http.MethodPut) require.Equal(t, v, updateRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.UpdateNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", updateRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_UpdateNodePool_AutoScale(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesNodePool{ ID: "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", Name: "name", Size: "s-1vcpu-1gb", Count: 4, Tags: []string{"tag-1", "tag-2"}, AutoScale: true, MinNodes: 0, MaxNodes: 10, } updateRequest := &KubernetesNodePoolUpdateRequest{ AutoScale: boolPtr(true), MinNodes: intPtr(0), MaxNodes: intPtr(10), } jBlob := ` { "node_pool": { "id": "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "size": "s-1vcpu-1gb", "count": 4, "name": "name", "tags": [ "tag-1", "tag-2" ], "auto_scale": true, "min_nodes": 0, "max_nodes": 10 } }` expectedReqJSON := `{"auto_scale":true,"min_nodes":0,"max_nodes":10} ` mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { buf := new(bytes.Buffer) buf.ReadFrom(r.Body) require.Equal(t, expectedReqJSON, buf.String()) v := new(KubernetesNodePoolUpdateRequest) err := json.NewDecoder(buf).Decode(v) require.NoError(t, err) testMethod(t, r, http.MethodPut) require.Equal(t, v, updateRequest) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.UpdateNodePool(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", updateRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestKubernetesClusters_DeleteNodePool(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := kubeSvc.DeleteNodePool(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a") require.NoError(t, err) } func TestKubernetesClusters_DeleteNode(t *testing.T) { t.Run("default", func(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a/nodes/8d91899c-node-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) require.Equal(t, "", r.URL.Query().Encode()) }) _, err := kubeSvc.DeleteNode(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "8d91899c-node-4a1a-acc5-deadbeefbb8a", nil) require.NoError(t, err) }) t.Run("drain", func(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a/nodes/8d91899c-node-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) require.Equal(t, "skip_drain=1", r.URL.Query().Encode()) }) _, err := kubeSvc.DeleteNode(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "8d91899c-node-4a1a-acc5-deadbeefbb8a", &KubernetesNodeDeleteRequest{ SkipDrain: true, }) require.NoError(t, err) }) t.Run("replace", func(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes mux.HandleFunc("/v2/kubernetes/clusters/deadbeef-dead-4aa5-beef-deadbeef347d/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a/nodes/8d91899c-node-4a1a-acc5-deadbeefbb8a", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) require.Equal(t, "replace=1", r.URL.Query().Encode()) }) _, err := kubeSvc.DeleteNode(ctx, "deadbeef-dead-4aa5-beef-deadbeef347d", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", "8d91899c-node-4a1a-acc5-deadbeefbb8a", &KubernetesNodeDeleteRequest{ Replace: true, }) require.NoError(t, err) }) } func TestKubernetesClusters_RecycleNodePoolNodes(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes recycleRequest := &KubernetesNodePoolRecycleNodesRequest{ Nodes: []string{"node1", "node2"}, } mux.HandleFunc("/v2/kubernetes/clusters/8d91899c-0739-4a1a-acc5-deadbeefbb8f/node_pools/8d91899c-nodepool-4a1a-acc5-deadbeefbb8a/recycle", func(w http.ResponseWriter, r *http.Request) { v := new(KubernetesNodePoolRecycleNodesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, recycleRequest) }) _, err := kubeSvc.RecycleNodePoolNodes(ctx, "8d91899c-0739-4a1a-acc5-deadbeefbb8f", "8d91899c-nodepool-4a1a-acc5-deadbeefbb8a", recycleRequest) require.NoError(t, err) } func TestKubernetesVersions_List(t *testing.T) { setup() defer teardown() kubeSvc := client.Kubernetes want := &KubernetesOptions{ Versions: []*KubernetesVersion{ {Slug: "1.10.0-gen0", KubernetesVersion: "1.10.0"}, }, Regions: []*KubernetesRegion{ {Name: "New York 3", Slug: "nyc3"}, }, Sizes: []*KubernetesNodeSize{ {Name: "c-8", Slug: "c-8"}, }, } jBlob := ` { "options": { "versions": [ { "slug": "1.10.0-gen0", "kubernetes_version": "1.10.0" } ], "regions": [ { "name": "New York 3", "slug": "nyc3" } ], "sizes": [ { "name": "c-8", "slug": "c-8" } ] } }` mux.HandleFunc("/v2/kubernetes/options", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := kubeSvc.GetOptions(ctx) require.NoError(t, err) require.Equal(t, want, got) } var maintenancePolicyDayTests = []struct { name string json string day KubernetesMaintenancePolicyDay valid bool }{ { name: "sunday", day: KubernetesMaintenanceDaySunday, json: `"sunday"`, valid: true, }, { name: "any", day: KubernetesMaintenanceDayAny, json: `"any"`, valid: true, }, { name: "invalid", day: 100, // invalid input json: `"invalid weekday (100)"`, valid: false, }, } func TestWeekday_UnmarshalJSON(t *testing.T) { for _, ts := range maintenancePolicyDayTests { t.Run(ts.name, func(t *testing.T) { var got KubernetesMaintenancePolicyDay err := json.Unmarshal([]byte(ts.json), &got) valid := err == nil if valid != ts.valid { t.Errorf("valid unmarshal case\n\tgot: %+v\n\twant : %+v", valid, ts.valid) } if valid && got != ts.day { t.Errorf("\ninput: %s\ngot : %+v\nwant : %+v\n", ts.day, got, ts.day) } }) } } func TestWeekday_MarshalJSON(t *testing.T) { for _, ts := range maintenancePolicyDayTests { t.Run(ts.name, func(t *testing.T) { out, err := json.Marshal(ts.day) valid := err == nil if valid != ts.valid { t.Errorf("valid marshal case\n\tgot: %+v\n\twant : %+v", valid, ts.valid) } if valid && ts.json != string(out) { t.Errorf("\ninput: %s\ngot : %+v\nwant : %+v\n", ts.day, string(out), ts.json) } }) } } godo-1.37.0/links.go000066400000000000000000000031711366527000000142020ustar00rootroot00000000000000package godo import ( "context" "net/url" "strconv" ) // 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.Next == "" } 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) } godo-1.37.0/links_test.go000066400000000000000000000075101366527000000152420ustar00rootroot00000000000000package godo import ( "encoding/json" "testing" ) var ( firstPageLinksJSONBlob = []byte(`{ "links": { "pages": { "last": "https://api.digitalocean.com/v2/droplets/?page=3", "next": "https://api.digitalocean.com/v2/droplets/?page=2" } } }`) otherPageLinksJSONBlob = []byte(`{ "links": { "pages": { "first": "https://api.digitalocean.com/v2/droplets/?page=1", "prev": "https://api.digitalocean.com/v2/droplets/?page=1", "last": "https://api.digitalocean.com/v2/droplets/?page=3", "next": "https://api.digitalocean.com/v2/droplets/?page=3" } } }`) lastPageLinksJSONBlob = []byte(`{ "links": { "pages": { "first": "https://api.digitalocean.com/v2/droplets/?page=1", "prev": "https://api.digitalocean.com/v2/droplets/?page=2" } } }`) projectsLastPageLinksJSONBlob = []byte(`{ "links": { "pages": { "first": "https://api.digitalocean.com/v2/projects?page=1", "prev": "https://api.digitalocean.com/v2/projects?page=2", "last": "https://api.digitalocean.com/v2/projects?page=3" } } }`) missingLinksJSONBlob = []byte(`{ }`) ) type godoList struct { Links Links `json:"links"` } func loadLinksJSON(t *testing.T, j []byte) Links { var list godoList err := json.Unmarshal(j, &list) if err != nil { t.Fatal(err) } return list.Links } func TestLinks_ParseFirst(t *testing.T) { links := loadLinksJSON(t, firstPageLinksJSONBlob) _, err := links.CurrentPage() if err != nil { t.Fatal(err) } r := &Response{Links: &links} checkCurrentPage(t, r, 1) if links.IsLastPage() { t.Fatalf("shouldn't be last page") } } func TestLinks_ParseMiddle(t *testing.T) { links := loadLinksJSON(t, otherPageLinksJSONBlob) _, err := links.CurrentPage() if err != nil { t.Fatal(err) } r := &Response{Links: &links} checkCurrentPage(t, r, 2) if links.IsLastPage() { t.Fatalf("shouldn't be last page") } } func TestLinks_ParseLast(t *testing.T) { links := loadLinksJSON(t, lastPageLinksJSONBlob) _, err := links.CurrentPage() if err != nil { t.Fatal(err) } r := &Response{Links: &links} checkCurrentPage(t, r, 3) if !links.IsLastPage() { t.Fatalf("expected last page") } } func TestLinks_ParseProjectsLast(t *testing.T) { links := loadLinksJSON(t, projectsLastPageLinksJSONBlob) _, err := links.CurrentPage() if err != nil { t.Fatal(err) } r := &Response{Links: &links} checkCurrentPage(t, r, 3) if !links.IsLastPage() { t.Fatalf("expected last page") } } func TestLinks_ParseMissing(t *testing.T) { links := loadLinksJSON(t, missingLinksJSONBlob) _, err := links.CurrentPage() if err != nil { t.Fatal(err) } r := &Response{Links: &links} checkCurrentPage(t, r, 1) } func TestLinks_ParseURL(t *testing.T) { type linkTest struct { name, url string expected int } linkTests := []linkTest{ { name: "prev", url: "https://api.digitalocean.com/v2/droplets/?page=1", expected: 1, }, { name: "last", url: "https://api.digitalocean.com/v2/droplets/?page=5", expected: 5, }, { name: "nexta", url: "https://api.digitalocean.com/v2/droplets/?page=2", expected: 2, }, } for _, lT := range linkTests { p, err := pageForURL(lT.url) if err != nil { t.Fatal(err) } if p != lT.expected { t.Errorf("expected page for '%s' to be '%d', was '%d'", lT.url, lT.expected, p) } } } func TestLinks_ParseEmptyString(t *testing.T) { type linkTest struct { name, url string expected int } linkTests := []linkTest{ { name: "none", url: "http://example.com", expected: 0, }, { name: "bad", url: "no url", expected: 0, }, { name: "empty", url: "", expected: 0, }, } for _, lT := range linkTests { _, err := pageForURL(lT.url) if err == nil { t.Fatalf("expected error for test '%s', but received none", lT.name) } } } godo-1.37.0/load_balancers.go000066400000000000000000000266751366527000000160310ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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. // Tags can only be provided upon the creation of a Load Balancer. 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"` Tags []string `json:"tags,omitempty"` RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"` EnableProxyProtocol bool `json:"enable_proxy_protocol,omitempty"` EnableBackendKeepalive bool `json:"enable_backend_keepalive,omitempty"` VPCUUID string `json:"vpc_uuid,omitempty"` } // String creates a human-readable description of a LoadBalancer. func (l LoadBalancer) String() string { return Stringify(l) } func (l LoadBalancer) URN() string { return ToURN("LoadBalancer", l.ID) } // AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer. // Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer. func (l LoadBalancer) AsRequest() *LoadBalancerRequest { r := LoadBalancerRequest{ Name: l.Name, Algorithm: l.Algorithm, ForwardingRules: append([]ForwardingRule(nil), l.ForwardingRules...), DropletIDs: append([]int(nil), l.DropletIDs...), Tag: l.Tag, RedirectHttpToHttps: l.RedirectHttpToHttps, EnableProxyProtocol: l.EnableProxyProtocol, EnableBackendKeepalive: l.EnableBackendKeepalive, HealthCheck: l.HealthCheck, VPCUUID: l.VPCUUID, } if l.HealthCheck != nil { r.HealthCheck = &HealthCheck{} *r.HealthCheck = *l.HealthCheck } if l.StickySessions != nil { r.StickySessions = &StickySessions{} *r.StickySessions = *l.StickySessions } if l.Region != nil { r.Region = l.Region.Slug } return &r } // 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"` Tags []string `json:"tags,omitempty"` RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"` EnableProxyProtocol bool `json:"enable_proxy_protocol,omitempty"` EnableBackendKeepalive bool `json:"enable_backend_keepalive,omitempty"` VPCUUID string `json:"vpc_uuid,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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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) } godo-1.37.0/load_balancers_test.go000066400000000000000000000557001366527000000170570ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" ) var lbListJSONResponse = ` { "load_balancers":[ { "id":"37e6be88-01ec-4ec7-9bc6-a514d4719057", "name":"example-lb-01", "ip":"46.214.185.203", "algorithm":"round_robin", "status":"active", "created_at":"2016-12-15T14:16:36Z", "forwarding_rules":[ { "entry_protocol":"https", "entry_port":443, "target_protocol":"http", "target_port":80, "certificate_id":"a-b-c" } ], "health_check":{ "protocol":"http", "port":80, "path":"/index.html", "check_interval_seconds":10, "response_timeout_seconds":5, "healthy_threshold":5, "unhealthy_threshold":3 }, "sticky_sessions":{ "type":"cookies", "cookie_name":"DO-LB", "cookie_ttl_seconds":5 }, "region":{ "name":"New York 1", "slug":"nyc1", "sizes":[ "512mb", "1gb", "2gb", "4gb", "8gb", "16gb" ], "features":[ "private_networking", "backups", "ipv6", "metadata", "storage" ], "available":true }, "droplet_ids":[ 2, 21 ] } ], "links":{ "pages":{ "last":"http://localhost:3001/v2/load_balancers?page=3&per_page=1", "next":"http://localhost:3001/v2/load_balancers?page=2&per_page=1" } }, "meta":{ "total":3 } } ` var lbCreateJSONResponse = ` { "load_balancer":{ "id":"8268a81c-fcf5-423e-a337-bbfe95817f23", "name":"example-lb-01", "ip":"", "algorithm":"round_robin", "status":"new", "created_at":"2016-12-15T14:19:09Z", "forwarding_rules":[ { "entry_protocol":"https", "entry_port":443, "target_protocol":"http", "target_port":80, "certificate_id":"a-b-c" }, { "entry_protocol":"https", "entry_port":444, "target_protocol":"https", "target_port":443, "tls_passthrough":true } ], "health_check":{ "protocol":"http", "port":80, "path":"/index.html", "check_interval_seconds":10, "response_timeout_seconds":5, "healthy_threshold":5, "unhealthy_threshold":3 }, "sticky_sessions":{ "type":"cookies", "cookie_name":"DO-LB", "cookie_ttl_seconds":5 }, "region":{ "name":"New York 1", "slug":"nyc1", "sizes":[ "512mb", "1gb", "2gb", "4gb", "8gb", "16gb" ], "features":[ "private_networking", "backups", "ipv6", "metadata", "storage" ], "available":true }, "tags": ["my-tag"], "droplet_ids":[ 2, 21 ], "redirect_http_to_https":true, "vpc_uuid":"880b7f98-f062-404d-b33c-458d545696f6" } } ` var lbGetJSONResponse = ` { "load_balancer":{ "id":"37e6be88-01ec-4ec7-9bc6-a514d4719057", "name":"example-lb-01", "ip":"46.214.185.203", "algorithm":"round_robin", "status":"active", "created_at":"2016-12-15T14:16:36Z", "forwarding_rules":[ { "entry_protocol":"https", "entry_port":443, "target_protocol":"http", "target_port":80, "certificate_id":"a-b-c" } ], "health_check":{ "protocol":"http", "port":80, "path":"/index.html", "check_interval_seconds":10, "response_timeout_seconds":5, "healthy_threshold":5, "unhealthy_threshold":3 }, "sticky_sessions":{ "type":"cookies", "cookie_name":"DO-LB", "cookie_ttl_seconds":5 }, "region":{ "name":"New York 1", "slug":"nyc1", "sizes":[ "512mb", "1gb", "2gb", "4gb", "8gb", "16gb" ], "features":[ "private_networking", "backups", "ipv6", "metadata", "storage" ], "available":true }, "droplet_ids":[ 2, 21 ] } } ` var lbUpdateJSONResponse = ` { "load_balancer":{ "id":"8268a81c-fcf5-423e-a337-bbfe95817f23", "name":"example-lb-01", "ip":"12.34.56.78", "algorithm":"least_connections", "status":"active", "created_at":"2016-12-15T14:19:09Z", "forwarding_rules":[ { "entry_protocol":"http", "entry_port":80, "target_protocol":"http", "target_port":80 }, { "entry_protocol":"https", "entry_port":443, "target_protocol":"http", "target_port":80, "certificate_id":"a-b-c" } ], "health_check":{ "protocol":"tcp", "port":80, "path":"", "check_interval_seconds":10, "response_timeout_seconds":5, "healthy_threshold":5, "unhealthy_threshold":3 }, "sticky_sessions":{ "type":"none" }, "region":{ "name":"New York 1", "slug":"nyc1", "sizes":[ "512mb", "1gb", "2gb", "4gb", "8gb", "16gb" ], "features":[ "private_networking", "backups", "ipv6", "metadata", "storage" ], "available":true }, "droplet_ids":[ 2, 21 ] } } ` func TestLoadBalancers_Get(t *testing.T) { setup() defer teardown() path := "/v2/load_balancers" loadBalancerId := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path = fmt.Sprintf("%s/%s", path, loadBalancerId) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, lbGetJSONResponse) }) loadBalancer, _, err := client.LoadBalancers.Get(ctx, loadBalancerId) if err != nil { t.Errorf("LoadBalancers.Get returned error: %v", err) } expected := &LoadBalancer{ ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057", Name: "example-lb-01", IP: "46.214.185.203", Algorithm: "round_robin", Status: "active", Created: "2016-12-15T14:16:36Z", ForwardingRules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", TlsPassthrough: false, }, }, HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/index.html", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, HealthyThreshold: 5, UnhealthyThreshold: 3, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "DO-LB", CookieTtlSeconds: 5, }, Region: &Region{ Slug: "nyc1", Name: "New York 1", Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"}, Available: true, Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"}, }, DropletIDs: []int{2, 21}, } assert.Equal(t, expected, loadBalancer) } func TestLoadBalancers_Create(t *testing.T) { setup() defer teardown() createRequest := &LoadBalancerRequest{ Name: "example-lb-01", Algorithm: "round_robin", Region: "nyc1", ForwardingRules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", }, }, HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/index.html", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, UnhealthyThreshold: 3, HealthyThreshold: 5, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "DO-LB", CookieTtlSeconds: 5, }, Tag: "my-tag", Tags: []string{"my-tag"}, DropletIDs: []int{2, 21}, RedirectHttpToHttps: true, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } path := "/v2/load_balancers" mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(LoadBalancerRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) assert.Equal(t, createRequest, v) fmt.Fprint(w, lbCreateJSONResponse) }) loadBalancer, _, err := client.LoadBalancers.Create(ctx, createRequest) if err != nil { t.Errorf("LoadBalancers.Create returned error: %v", err) } expected := &LoadBalancer{ ID: "8268a81c-fcf5-423e-a337-bbfe95817f23", Name: "example-lb-01", Algorithm: "round_robin", Status: "new", Created: "2016-12-15T14:19:09Z", ForwardingRules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", TlsPassthrough: false, }, { EntryProtocol: "https", EntryPort: 444, TargetProtocol: "https", TargetPort: 443, CertificateID: "", TlsPassthrough: true, }, }, HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/index.html", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, HealthyThreshold: 5, UnhealthyThreshold: 3, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "DO-LB", CookieTtlSeconds: 5, }, Region: &Region{ Slug: "nyc1", Name: "New York 1", Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"}, Available: true, Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"}, }, Tags: []string{"my-tag"}, DropletIDs: []int{2, 21}, RedirectHttpToHttps: true, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } assert.Equal(t, expected, loadBalancer) } func TestLoadBalancers_Update(t *testing.T) { setup() defer teardown() updateRequest := &LoadBalancerRequest{ Name: "example-lb-01", Algorithm: "least_connections", Region: "nyc1", ForwardingRules: []ForwardingRule{ { EntryProtocol: "http", EntryPort: 80, TargetProtocol: "http", TargetPort: 80, }, { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", }, }, HealthCheck: &HealthCheck{ Protocol: "tcp", Port: 80, Path: "", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, UnhealthyThreshold: 3, HealthyThreshold: 5, }, StickySessions: &StickySessions{ Type: "none", }, DropletIDs: []int{2, 21}, } path := "/v2/load_balancers" loadBalancerId := "8268a81c-fcf5-423e-a337-bbfe95817f23" path = fmt.Sprintf("%s/%s", path, loadBalancerId) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(LoadBalancerRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, "PUT") assert.Equal(t, updateRequest, v) fmt.Fprint(w, lbUpdateJSONResponse) }) loadBalancer, _, err := client.LoadBalancers.Update(ctx, loadBalancerId, updateRequest) if err != nil { t.Errorf("LoadBalancers.Update returned error: %v", err) } expected := &LoadBalancer{ ID: "8268a81c-fcf5-423e-a337-bbfe95817f23", Name: "example-lb-01", IP: "12.34.56.78", Algorithm: "least_connections", Status: "active", Created: "2016-12-15T14:19:09Z", ForwardingRules: []ForwardingRule{ { EntryProtocol: "http", EntryPort: 80, TargetProtocol: "http", TargetPort: 80, }, { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", }, }, HealthCheck: &HealthCheck{ Protocol: "tcp", Port: 80, Path: "", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, UnhealthyThreshold: 3, HealthyThreshold: 5, }, StickySessions: &StickySessions{ Type: "none", }, Region: &Region{ Slug: "nyc1", Name: "New York 1", Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"}, Available: true, Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"}, }, DropletIDs: []int{2, 21}, } assert.Equal(t, expected, loadBalancer) } func TestLoadBalancers_List(t *testing.T) { setup() defer teardown() path := "/v2/load_balancers" mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, lbListJSONResponse) }) loadBalancers, resp, err := client.LoadBalancers.List(ctx, nil) if err != nil { t.Errorf("LoadBalancers.List returned error: %v", err) } expectedLBs := []LoadBalancer{ { ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057", Name: "example-lb-01", IP: "46.214.185.203", Algorithm: "round_robin", Status: "active", Created: "2016-12-15T14:16:36Z", ForwardingRules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 443, TargetProtocol: "http", TargetPort: 80, CertificateID: "a-b-c", }, }, HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/index.html", CheckIntervalSeconds: 10, ResponseTimeoutSeconds: 5, HealthyThreshold: 5, UnhealthyThreshold: 3, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "DO-LB", CookieTtlSeconds: 5, }, Region: &Region{ Slug: "nyc1", Name: "New York 1", Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"}, Available: true, Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"}, }, DropletIDs: []int{2, 21}, }, } assert.Equal(t, expectedLBs, loadBalancers) expectedMeta := &Meta{Total: 3} assert.Equal(t, expectedMeta, resp.Meta) } func TestLoadBalancers_List_Pagination(t *testing.T) { setup() defer teardown() path := "/v2/load_balancers" mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) testFormValues(t, r, map[string]string{"page": "2"}) fmt.Fprint(w, lbListJSONResponse) }) opts := &ListOptions{Page: 2} _, resp, err := client.LoadBalancers.List(ctx, opts) if err != nil { t.Errorf("LoadBalancers.List returned error: %v", err) } assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=2&per_page=1", resp.Links.Pages.Next) assert.Equal(t, "http://localhost:3001/v2/load_balancers?page=3&per_page=1", resp.Links.Pages.Last) } func TestLoadBalancers_Delete(t *testing.T) { setup() defer teardown() lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path := "/v2/load_balancers" path = fmt.Sprintf("%s/%s", path, lbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.LoadBalancers.Delete(ctx, lbID) if err != nil { t.Errorf("LoadBalancers.Delete returned error: %v", err) } } func TestLoadBalancers_AddDroplets(t *testing.T) { setup() defer teardown() dropletIdsRequest := &dropletIDsRequest{ IDs: []int{42, 44}, } lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(dropletIDsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) assert.Equal(t, dropletIdsRequest, v) fmt.Fprint(w, nil) }) _, err := client.LoadBalancers.AddDroplets(ctx, lbID, dropletIdsRequest.IDs...) if err != nil { t.Errorf("LoadBalancers.AddDroplets returned error: %v", err) } } func TestLoadBalancers_RemoveDroplets(t *testing.T) { setup() defer teardown() dropletIdsRequest := &dropletIDsRequest{ IDs: []int{2, 21}, } lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(dropletIDsRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodDelete) assert.Equal(t, dropletIdsRequest, v) fmt.Fprint(w, nil) }) _, err := client.LoadBalancers.RemoveDroplets(ctx, lbID, dropletIdsRequest.IDs...) if err != nil { t.Errorf("LoadBalancers.RemoveDroplets returned error: %v", err) } } func TestLoadBalancers_AddForwardingRules(t *testing.T) { setup() defer teardown() frr := &forwardingRulesRequest{ Rules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 444, TargetProtocol: "http", TargetPort: 81, CertificateID: "b2abc00f-d3c4-426c-9f0b-b2f7a3ff7527", }, { EntryProtocol: "tcp", EntryPort: 8080, TargetProtocol: "tcp", TargetPort: 8081, }, }, } lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(forwardingRulesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) assert.Equal(t, frr, v) fmt.Fprint(w, nil) }) _, err := client.LoadBalancers.AddForwardingRules(ctx, lbID, frr.Rules...) if err != nil { t.Errorf("LoadBalancers.AddForwardingRules returned error: %v", err) } } func TestLoadBalancers_RemoveForwardingRules(t *testing.T) { setup() defer teardown() frr := &forwardingRulesRequest{ Rules: []ForwardingRule{ { EntryProtocol: "https", EntryPort: 444, TargetProtocol: "http", TargetPort: 81, }, { EntryProtocol: "tcp", EntryPort: 8080, TargetProtocol: "tcp", TargetPort: 8081, }, }, } lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057" path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID) mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { v := new(forwardingRulesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodDelete) assert.Equal(t, frr, v) fmt.Fprint(w, nil) }) _, err := client.LoadBalancers.RemoveForwardingRules(ctx, lbID, frr.Rules...) if err != nil { t.Errorf("LoadBalancers.RemoveForwardingRules returned error: %v", err) } } func TestLoadBalancers_AsRequest(t *testing.T) { lb := &LoadBalancer{ ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057", Name: "test-loadbalancer", IP: "10.0.0.1", Algorithm: "least_connections", Status: "active", Created: "2011-06-24T12:00:00Z", HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/ping", CheckIntervalSeconds: 30, ResponseTimeoutSeconds: 10, HealthyThreshold: 3, UnhealthyThreshold: 3, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "nomnom", CookieTtlSeconds: 32, }, Region: &Region{ Slug: "lon1", }, RedirectHttpToHttps: true, EnableProxyProtocol: true, EnableBackendKeepalive: true, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } lb.DropletIDs = make([]int, 1, 2) lb.DropletIDs[0] = 12345 lb.ForwardingRules = make([]ForwardingRule, 1, 2) lb.ForwardingRules[0] = ForwardingRule{ EntryProtocol: "http", EntryPort: 80, TargetProtocol: "http", TargetPort: 80, } want := &LoadBalancerRequest{ Name: "test-loadbalancer", Algorithm: "least_connections", Region: "lon1", ForwardingRules: []ForwardingRule{ForwardingRule{ EntryProtocol: "http", EntryPort: 80, TargetProtocol: "http", TargetPort: 80, }}, HealthCheck: &HealthCheck{ Protocol: "http", Port: 80, Path: "/ping", CheckIntervalSeconds: 30, ResponseTimeoutSeconds: 10, HealthyThreshold: 3, UnhealthyThreshold: 3, }, StickySessions: &StickySessions{ Type: "cookies", CookieName: "nomnom", CookieTtlSeconds: 32, }, DropletIDs: []int{12345}, RedirectHttpToHttps: true, EnableProxyProtocol: true, EnableBackendKeepalive: true, VPCUUID: "880b7f98-f062-404d-b33c-458d545696f6", } r := lb.AsRequest() assert.Equal(t, want, r) assert.False(t, r.HealthCheck == lb.HealthCheck, "HealthCheck points to same struct") assert.False(t, r.StickySessions == lb.StickySessions, "StickySessions points to same struct") r.DropletIDs = append(r.DropletIDs, 54321) r.ForwardingRules = append(r.ForwardingRules, ForwardingRule{ EntryProtocol: "https", EntryPort: 443, TargetProtocol: "https", TargetPort: 443, TlsPassthrough: true, }) // Check that original LoadBalancer hasn't changed lb.DropletIDs = append(lb.DropletIDs, 13579) lb.ForwardingRules = append(lb.ForwardingRules, ForwardingRule{ EntryProtocol: "tcp", EntryPort: 587, TargetProtocol: "tcp", TargetPort: 587, }) assert.Equal(t, []int{12345, 54321}, r.DropletIDs) assert.Equal(t, []ForwardingRule{ ForwardingRule{ EntryProtocol: "http", EntryPort: 80, TargetProtocol: "http", TargetPort: 80, }, ForwardingRule{ EntryProtocol: "https", EntryPort: 443, TargetProtocol: "https", TargetPort: 443, TlsPassthrough: true, }, }, r.ForwardingRules) } godo-1.37.0/meta.go000066400000000000000000000001651366527000000140100ustar00rootroot00000000000000package godo // Meta describes generic information about a response. type Meta struct { Total int `json:"total"` } godo-1.37.0/projects.go000066400000000000000000000213201366527000000147070ustar00rootroot00000000000000package godo import ( "context" "encoding/json" "fmt" "net/http" "path" ) const ( // DefaultProject is the ID you should use if you are working with your // default project. DefaultProject = "default" projectsBasePath = "/v2/projects" ) // ProjectsService is an interface for creating and managing Projects with the DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2/#projects type ProjectsService interface { List(context.Context, *ListOptions) ([]Project, *Response, error) GetDefault(context.Context) (*Project, *Response, error) Get(context.Context, string) (*Project, *Response, error) Create(context.Context, *CreateProjectRequest) (*Project, *Response, error) Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error) Delete(context.Context, string) (*Response, error) ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error) AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error) } // ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API. type ProjectsServiceOp struct { client *Client } // Project represents a DigitalOcean Project configuration. type Project struct { ID string `json:"id"` OwnerUUID string `json:"owner_uuid"` OwnerID uint64 `json:"owner_id"` Name string `json:"name"` Description string `json:"description"` Purpose string `json:"purpose"` Environment string `json:"environment"` IsDefault bool `json:"is_default"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // String creates a human-readable description of a Project. func (p Project) String() string { return Stringify(p) } // CreateProjectRequest represents the request to create a new project. type CreateProjectRequest struct { Name string `json:"name"` Description string `json:"description"` Purpose string `json:"purpose"` Environment string `json:"environment"` } // UpdateProjectRequest represents the request to update project information. // This type expects certain attribute types, but is built this way to allow // nil values as well. See `updateProjectRequest` for the "real" types. type UpdateProjectRequest struct { Name interface{} Description interface{} Purpose interface{} Environment interface{} IsDefault interface{} } type updateProjectRequest struct { Name *string `json:"name"` Description *string `json:"description"` Purpose *string `json:"purpose"` Environment *string `json:"environment"` IsDefault *bool `json:"is_default"` } // MarshalJSON takes an UpdateRequest and converts it to the "typed" request // which is sent to the projects API. This is a PATCH request, which allows // partial attributes, so `null` values are OK. func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) { d := &updateProjectRequest{} if str, ok := upr.Name.(string); ok { d.Name = &str } if str, ok := upr.Description.(string); ok { d.Description = &str } if str, ok := upr.Purpose.(string); ok { d.Purpose = &str } if str, ok := upr.Environment.(string); ok { d.Environment = &str } if val, ok := upr.IsDefault.(bool); ok { d.IsDefault = &val } return json.Marshal(d) } type assignResourcesRequest struct { Resources []string `json:"resources"` } // ProjectResource is the projects API's representation of a resource. type ProjectResource struct { URN string `json:"urn"` AssignedAt string `json:"assigned_at"` Links *ProjectResourceLinks `json:"links"` Status string `json:"status,omitempty"` } // ProjetResourceLinks specify the link for more information about the resource. type ProjectResourceLinks struct { Self string `json:"self"` } type projectsRoot struct { Projects []Project `json:"projects"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } type projectRoot struct { Project *Project `json:"project"` } type projectResourcesRoot struct { Resources []ProjectResource `json:"resources"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } var _ ProjectsService = &ProjectsServiceOp{} // List Projects. func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) { path, err := addOptions(projectsBasePath, opts) if err != nil { return nil, nil, err } req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(projectsRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Projects, resp, err } // GetDefault project. func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) { return p.getHelper(ctx, "default") } // Get retrieves a single project by its ID. func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) { return p.getHelper(ctx, projectID) } // Create a new project. func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) { req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr) if err != nil { return nil, nil, err } root := new(projectRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Project, resp, err } // Update an existing project. func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) { path := path.Join(projectsBasePath, projectID) req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur) if err != nil { return nil, nil, err } root := new(projectRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Project, resp, err } // Delete an existing project. You cannot have any resources in a project // before deleting it. See the API documentation for more details. func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) { path := path.Join(projectsBasePath, projectID) req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } return p.client.Do(ctx, req, nil) } // ListResources lists all resources in a project. func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) { basePath := path.Join(projectsBasePath, projectID, "resources") path, err := addOptions(basePath, opts) if err != nil { return nil, nil, err } req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(projectResourcesRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Resources, resp, err } // AssignResources assigns one or more resources to a project. AssignResources // accepts resources in two possible formats: // 1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}` // 2. A valid DO URN as a string, like "do:droplet:1234" // // There is no unassign. To move a resource to another project, just assign // it to that other project. func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) { path := path.Join(projectsBasePath, projectID, "resources") ar := &assignResourcesRequest{ Resources: make([]string, len(resources)), } for i, resource := range resources { switch resource := resource.(type) { case ResourceWithURN: ar.Resources[i] = resource.URN() case string: ar.Resources[i] = resource default: return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource) } } req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar) if err != nil { return nil, nil, err } root := new(projectResourcesRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } return root.Resources, resp, err } func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) { path := path.Join(projectsBasePath, projectID) req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(projectRoot) resp, err := p.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Project, resp, err } godo-1.37.0/projects_test.go000066400000000000000000000363471366527000000157650ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "io/ioutil" "net/http" "reflect" "strings" "testing" ) func TestProjects_List(t *testing.T) { setup() defer teardown() expectedProjects := []Project{ { ID: "project-1", Name: "project-1", }, { ID: "project-2", Name: "project-2", }, } mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) resp, _ := json.Marshal(expectedProjects) fmt.Fprint(w, fmt.Sprintf(`{"projects":%s, "meta": {"total": 2}}`, string(resp))) }) projects, resp, err := client.Projects.List(ctx, nil) if err != nil { t.Errorf("Projects.List returned error: %v", err) } if !reflect.DeepEqual(projects, expectedProjects) { t.Errorf("Projects.List returned projects %+v, expected %+v", projects, expectedProjects) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Projects.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestProjects_ListWithMultiplePages(t *testing.T) { setup() defer teardown() mockResp := ` { "projects": [ { "uuid": "project-1", "name": "project-1" }, { "uuid": "project-2", "name": "project-2" } ], "links": { "pages": { "next": "http://example.com/v2/projects?page=2" } } }` mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, mockResp) }) _, resp, err := client.Projects.List(ctx, nil) if err != nil { t.Errorf("Projects.List returned error: %v", err) } checkCurrentPage(t, resp, 1) } func TestProjects_ListWithPageNumber(t *testing.T) { setup() defer teardown() mockResp := ` { "projects": [ { "uuid": "project-1", "name": "project-1" }, { "uuid": "project-2", "name": "project-2" } ], "links": { "pages": { "next": "http://example.com/v2/projects?page=3", "prev": "http://example.com/v2/projects?page=1", "last": "http://example.com/v2/projects?page=3", "first": "http://example.com/v2/projects?page=1" } } }` mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, mockResp) }) _, resp, err := client.Projects.List(ctx, &ListOptions{Page: 2}) if err != nil { t.Errorf("Projects.List returned error: %v", err) } checkCurrentPage(t, resp, 2) } func TestProjects_GetDefault(t *testing.T) { setup() defer teardown() project := &Project{ ID: "project-1", Name: "project-1", } mux.HandleFunc("/v2/projects/default", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) resp, _ := json.Marshal(project) fmt.Fprint(w, fmt.Sprintf(`{"project":%s}`, string(resp))) }) resp, _, err := client.Projects.GetDefault(ctx) if err != nil { t.Errorf("Projects.GetDefault returned error: %v", err) } if !reflect.DeepEqual(resp, project) { t.Errorf("Projects.GetDefault returned %+v, expected %+v", resp, project) } } func TestProjects_GetWithUUID(t *testing.T) { setup() defer teardown() project := &Project{ ID: "project-1", Name: "project-1", } mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) resp, _ := json.Marshal(project) fmt.Fprint(w, fmt.Sprintf(`{"project":%s}`, string(resp))) }) resp, _, err := client.Projects.Get(ctx, "project-1") if err != nil { t.Errorf("Projects.Get returned error: %v", err) } if !reflect.DeepEqual(resp, project) { t.Errorf("Projects.Get returned %+v, expected %+v", resp, project) } } func TestProjects_Create(t *testing.T) { setup() defer teardown() createRequest := &CreateProjectRequest{ Name: "my project", Description: "for my stuff", Purpose: "Just trying out DigitalOcean", Environment: "Production", } createResp := &Project{ ID: "project-id", Name: createRequest.Name, Description: createRequest.Description, Purpose: createRequest.Purpose, Environment: createRequest.Environment, } mux.HandleFunc("/v2/projects", func(w http.ResponseWriter, r *http.Request) { v := new(CreateProjectRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } resp, _ := json.Marshal(createResp) fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp))) }) project, _, err := client.Projects.Create(ctx, createRequest) if err != nil { t.Errorf("Projects.Create returned error: %v", err) } if !reflect.DeepEqual(project, createResp) { t.Errorf("Projects.Create returned %+v, expected %+v", project, createResp) } } func TestProjects_UpdateWithOneAttribute(t *testing.T) { setup() defer teardown() updateRequest := &UpdateProjectRequest{ Name: "my-great-project", } updateResp := &Project{ ID: "project-id", Name: updateRequest.Name.(string), Description: "some-other-description", Purpose: "some-other-purpose", Environment: "some-other-env", IsDefault: false, } mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) { reqBytes, respErr := ioutil.ReadAll(r.Body) if respErr != nil { t.Error("projects mock didn't work") } req := strings.TrimSuffix(string(reqBytes), "\n") expectedReq := `{"name":"my-great-project","description":null,"purpose":null,"environment":null,"is_default":null}` if req != expectedReq { t.Errorf("projects req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req) } resp, _ := json.Marshal(updateResp) fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp))) }) project, _, err := client.Projects.Update(ctx, "project-1", updateRequest) if err != nil { t.Errorf("Projects.Update returned error: %v", err) } if !reflect.DeepEqual(project, updateResp) { t.Errorf("Projects.Update returned %+v, expected %+v", project, updateResp) } } func TestProjects_UpdateWithAllAttributes(t *testing.T) { setup() defer teardown() updateRequest := &UpdateProjectRequest{ Name: "my-great-project", Description: "some-description", Purpose: "some-purpose", Environment: "some-env", IsDefault: true, } updateResp := &Project{ ID: "project-id", Name: updateRequest.Name.(string), Description: updateRequest.Description.(string), Purpose: updateRequest.Purpose.(string), Environment: updateRequest.Environment.(string), IsDefault: updateRequest.IsDefault.(bool), } mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) { reqBytes, respErr := ioutil.ReadAll(r.Body) if respErr != nil { t.Error("projects mock didn't work") } req := strings.TrimSuffix(string(reqBytes), "\n") expectedReq := `{"name":"my-great-project","description":"some-description","purpose":"some-purpose","environment":"some-env","is_default":true}` if req != expectedReq { t.Errorf("projects req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req) } resp, _ := json.Marshal(updateResp) fmt.Fprintf(w, fmt.Sprintf(`{"project":%s}`, string(resp))) }) project, _, err := client.Projects.Update(ctx, "project-1", updateRequest) if err != nil { t.Errorf("Projects.Update returned error: %v", err) } if !reflect.DeepEqual(project, updateResp) { t.Errorf("Projects.Update returned %+v, expected %+v", project, updateResp) } } func TestProjects_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/projects/project-1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Projects.Delete(ctx, "project-1") if err != nil { t.Errorf("Projects.Delete returned error: %v", err) } } func TestProjects_ListResources(t *testing.T) { setup() defer teardown() expectedResources := []ProjectResource{ { URN: "do:droplet:1", AssignedAt: "2018-09-27 00:00:00", Links: &ProjectResourceLinks{ Self: "http://example.com/v2/droplets/1", }, }, { URN: "do:floatingip:1.2.3.4", AssignedAt: "2018-09-27 00:00:00", Links: &ProjectResourceLinks{ Self: "http://example.com/v2/floating_ips/1.2.3.4", }, }, } mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) resp, _ := json.Marshal(expectedResources) fmt.Fprint(w, fmt.Sprintf(`{"resources":%s, "meta": {"total": 2}}`, string(resp))) }) resources, resp, err := client.Projects.ListResources(ctx, "project-1", nil) if err != nil { t.Errorf("Projects.List returned error: %v", err) } if !reflect.DeepEqual(resources, expectedResources) { t.Errorf("Projects.ListResources returned resources %+v, expected %+v", resources, expectedResources) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Projects.ListResources returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestProjects_ListResourcesWithMultiplePages(t *testing.T) { setup() defer teardown() mockResp := ` { "resources": [ { "urn": "do:droplet:1", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/droplets/1" } }, { "urn": "do:floatingip:1.2.3.4", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/floating_ips/1.2.3.4" } } ], "links": { "pages": { "next": "http://example.com/v2/projects/project-1/resources?page=2" } } }` mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, mockResp) }) _, resp, err := client.Projects.ListResources(ctx, "project-1", nil) if err != nil { t.Errorf("Projects.ListResources returned error: %v", err) } checkCurrentPage(t, resp, 1) } func TestProjects_ListResourcesWithPageNumber(t *testing.T) { setup() defer teardown() mockResp := ` { "resources": [ { "urn": "do:droplet:1", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/droplets/1" } }, { "urn": "do:floatingip:1.2.3.4", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/floating_ips/1.2.3.4" } } ], "links": { "pages": { "next": "http://example.com/v2/projects/project-1/resources?page=3", "prev": "http://example.com/v2/projects/project-1/resources?page=1", "last": "http://example.com/v2/projects/project-1/resources?page=3", "first": "http://example.com/v2/projects/project-1/resources?page=1" } } }` mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, mockResp) }) _, resp, err := client.Projects.ListResources(ctx, "project-1", &ListOptions{Page: 2}) if err != nil { t.Errorf("Projects.ListResources returned error: %v", err) } checkCurrentPage(t, resp, 2) } func TestProjects_AssignFleetResourcesWithTypes(t *testing.T) { setup() defer teardown() assignableResources := []interface{}{ &Droplet{ID: 1234}, &FloatingIP{IP: "1.2.3.4"}, } mockResp := ` { "resources": [ { "urn": "do:droplet:1234", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/droplets/1" } }, { "urn": "do:floatingip:1.2.3.4", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/floating_ips/1.2.3.4" } } ] }` mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) reqBytes, respErr := ioutil.ReadAll(r.Body) if respErr != nil { t.Error("projects mock didn't work") } req := strings.TrimSuffix(string(reqBytes), "\n") expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}` if req != expectedReq { t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req) } fmt.Fprint(w, mockResp) }) _, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...) if err != nil { t.Errorf("Projects.AssignResources returned error: %v", err) } } func TestProjects_AssignFleetResourcesWithStrings(t *testing.T) { setup() defer teardown() assignableResources := []interface{}{ "do:droplet:1234", "do:floatingip:1.2.3.4", } mockResp := ` { "resources": [ { "urn": "do:droplet:1234", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/droplets/1" } }, { "urn": "do:floatingip:1.2.3.4", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/floating_ips/1.2.3.4" } } ] }` mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) reqBytes, respErr := ioutil.ReadAll(r.Body) if respErr != nil { t.Error("projects mock didn't work") } req := strings.TrimSuffix(string(reqBytes), "\n") expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}` if req != expectedReq { t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req) } fmt.Fprint(w, mockResp) }) _, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...) if err != nil { t.Errorf("Projects.AssignResources returned error: %v", err) } } func TestProjects_AssignFleetResourcesWithStringsAndTypes(t *testing.T) { setup() defer teardown() assignableResources := []interface{}{ "do:droplet:1234", &FloatingIP{IP: "1.2.3.4"}, } mockResp := ` { "resources": [ { "urn": "do:droplet:1234", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/droplets/1" } }, { "urn": "do:floatingip:1.2.3.4", "assigned_at": "2018-09-27 00:00:00", "links": { "self": "http://example.com/v2/floating_ips/1.2.3.4" } } ] }` mux.HandleFunc("/v2/projects/project-1/resources", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) reqBytes, respErr := ioutil.ReadAll(r.Body) if respErr != nil { t.Error("projects mock didn't work") } req := strings.TrimSuffix(string(reqBytes), "\n") expectedReq := `{"resources":["do:droplet:1234","do:floatingip:1.2.3.4"]}` if req != expectedReq { t.Errorf("projects assign req didn't match up:\n expected %+v\n got %+v\n", expectedReq, req) } fmt.Fprint(w, mockResp) }) _, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...) if err != nil { t.Errorf("Projects.AssignResources returned error: %v", err) } } func TestProjects_AssignFleetResourcesWithTypeWithoutURNReturnsError(t *testing.T) { setup() defer teardown() type fakeType struct{} assignableResources := []interface{}{ fakeType{}, } _, _, err := client.Projects.AssignResources(ctx, "project-1", assignableResources...) if err == nil { t.Errorf("expected Projects.AssignResources to error, but it did not") } if err.Error() != "godo.fakeType must either be a string or have a valid URN method" { t.Errorf("Projects.AssignResources returned the wrong error: %v", err) } } godo-1.37.0/regions.go000066400000000000000000000030341366527000000145260ustar00rootroot00000000000000package godo import ( "context" "net/http" ) // 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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } return root.Regions, resp, err } godo-1.37.0/regions_test.go000066400000000000000000000045231366527000000155710ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" ) func TestRegions_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "regions": [ { "slug": "1" }, { "slug": "2" } ], "meta": { "total": 2 } }`) }) regions, resp, err := client.Regions.List(ctx, nil) if err != nil { t.Errorf("Regions.List returned error: %v", err) } expectedRegions := []Region{{Slug: "1"}, {Slug: "2"}} if !reflect.DeepEqual(regions, expectedRegions) { t.Errorf("Regions.List returned regions %+v, expected %+v", regions, expectedRegions) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Regions.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestRegions_ListRegionsMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"regions": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/regions/?page=2"}}}`) }) _, resp, err := client.Regions.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestRegions_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "regions": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/regions/?page=3", "prev":"http://example.com/v2/regions/?page=1", "last":"http://example.com/v2/regions/?page=3", "first":"http://example.com/v2/regions/?page=1" } } }` mux.HandleFunc("/v2/regions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Regions.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestRegion_String(t *testing.T) { region := &Region{ Slug: "region", Name: "Region", Sizes: []string{"1", "2"}, Available: true, } stringified := region.String() expected := `godo.Region{Slug:"region", Name:"Region", Sizes:["1" "2"], Available:true}` if expected != stringified { t.Errorf("Region.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/registry.go000066400000000000000000000174421366527000000147400ustar00rootroot00000000000000package godo import ( "bytes" "context" "fmt" "net/http" "net/url" "strconv" "time" ) const ( registryPath = "/v2/registry" // RegistryServer is the hostname of the DigitalOcean registry service RegistryServer = "registry.digitalocean.com" ) // RegistryService is an interface for interfacing with the Registry endpoints // of the DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2#registry type RegistryService interface { Create(context.Context, *RegistryCreateRequest) (*Registry, *Response, error) Get(context.Context) (*Registry, *Response, error) Delete(context.Context) (*Response, error) DockerCredentials(context.Context, *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) ListRepositories(context.Context, string, *ListOptions) ([]*Repository, *Response, error) ListRepositoryTags(context.Context, string, string, *ListOptions) ([]*RepositoryTag, *Response, error) DeleteTag(context.Context, string, string, string) (*Response, error) DeleteManifest(context.Context, string, string, string) (*Response, error) } var _ RegistryService = &RegistryServiceOp{} // RegistryServiceOp handles communication with Registry methods of the DigitalOcean API. type RegistryServiceOp struct { client *Client } // RegistryCreateRequest represents a request to create a registry. type RegistryCreateRequest struct { Name string `json:"name,omitempty"` } // RegistryDockerCredentialsRequest represents a request to retrieve docker // credentials for a registry. type RegistryDockerCredentialsRequest struct { ReadWrite bool `json:"read_write"` ExpirySeconds *int `json:"expiry_seconds,omitempty"` } // Registry represents a registry. type Registry struct { Name string `json:"name,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` } // Repository represents a repository type Repository struct { RegistryName string `json:"registry_name,omitempty"` Name string `json:"name,omitempty"` LatestTag *RepositoryTag `json:"latest_tag,omitempty"` TagCount uint64 `json:"tag_count,omitempty"` } // RepositoryTag represents a repository tag type RepositoryTag struct { RegistryName string `json:"registry_name,omitempty"` Repository string `json:"repository,omitempty"` Tag string `json:"tag,omitempty"` ManifestDigest string `json:"manifest_digest,omitempty"` CompressedSizeBytes uint64 `json:"compressed_size_bytes,omitempty"` SizeBytes uint64 `json:"size_bytes,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } type registryRoot struct { Registry *Registry `json:"registry,omitempty"` } type repositoriesRoot struct { Repositories []*Repository `json:"repositories,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } type repositoryTagsRoot struct { Tags []*RepositoryTag `json:"tags,omitempty"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta"` } // Get retrieves the details of a Registry. func (svc *RegistryServiceOp) Get(ctx context.Context) (*Registry, *Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodGet, registryPath, nil) if err != nil { return nil, nil, err } root := new(registryRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Registry, resp, nil } // Create creates a registry. func (svc *RegistryServiceOp) Create(ctx context.Context, create *RegistryCreateRequest) (*Registry, *Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodPost, registryPath, create) if err != nil { return nil, nil, err } root := new(registryRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.Registry, resp, nil } // Delete deletes a registry. There is no way to recover a registry once it has // been destroyed. func (svc *RegistryServiceOp) Delete(ctx context.Context) (*Response, error) { req, err := svc.client.NewRequest(ctx, http.MethodDelete, registryPath, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // DockerCredentials is the content of a Docker config file // that is used by the docker CLI // See: https://docs.docker.com/engine/reference/commandline/cli/#configjson-properties type DockerCredentials struct { DockerConfigJSON []byte } // DockerCredentials retrieves a Docker config file containing the registry's credentials. func (svc *RegistryServiceOp) DockerCredentials(ctx context.Context, request *RegistryDockerCredentialsRequest) (*DockerCredentials, *Response, error) { path := fmt.Sprintf("%s/%s", registryPath, "docker-credentials") req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } q := req.URL.Query() q.Add("read_write", strconv.FormatBool(request.ReadWrite)) if request.ExpirySeconds != nil { q.Add("expiry_seconds", strconv.Itoa(*request.ExpirySeconds)) } req.URL.RawQuery = q.Encode() var buf bytes.Buffer resp, err := svc.client.Do(ctx, req, &buf) if err != nil { return nil, resp, err } dc := &DockerCredentials{ DockerConfigJSON: buf.Bytes(), } return dc, resp, nil } // ListRepositories returns a list of the Repositories visible with the registry's credentials. func (svc *RegistryServiceOp) ListRepositories(ctx context.Context, registry string, opts *ListOptions) ([]*Repository, *Response, error) { path := fmt.Sprintf("%s/%s/repositories", registryPath, registry) path, err := addOptions(path, opts) 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(repositoriesRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Repositories, resp, nil } // ListRepositoryTags returns a list of the RepositoryTags available within the given repository. func (svc *RegistryServiceOp) ListRepositoryTags(ctx context.Context, registry, repository string, opts *ListOptions) ([]*RepositoryTag, *Response, error) { path := fmt.Sprintf("%s/%s/repositories/%s/tags", registryPath, registry, url.PathEscape(repository)) path, err := addOptions(path, opts) 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(repositoryTagsRoot) resp, err := svc.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.Tags, resp, nil } // DeleteTag deletes a tag within a given repository. func (svc *RegistryServiceOp) DeleteTag(ctx context.Context, registry, repository, tag string) (*Response, error) { path := fmt.Sprintf("%s/%s/repositories/%s/tags/%s", registryPath, registry, url.PathEscape(repository), tag) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } // DeleteManifest deletes a manifest by its digest within a given repository. func (svc *RegistryServiceOp) DeleteManifest(ctx context.Context, registry, repository, digest string) (*Response, error) { path := fmt.Sprintf("%s/%s/repositories/%s/digests/%s", registryPath, registry, url.PathEscape(repository), digest) req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := svc.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } godo-1.37.0/registry_test.go000066400000000000000000000211061366527000000157670ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( testRegistry = "test-registry" testRepository = "test/repository" testEncodedRepository = "test%2Frepository" testTag = "test-tag" testDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" testCompressedSize = 2789669 testSize = 5843968 ) var ( testTime = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC) testTimeString = testTime.Format(time.RFC3339) ) func TestRegistry_Create(t *testing.T) { setup() defer teardown() want := &Registry{ Name: testRegistry, CreatedAt: testTime, } createRequest := &RegistryCreateRequest{ Name: want.Name, } createResponseJSON := ` { "registry": { "name": "` + testRegistry + `", "created_at": "` + testTimeString + `" } }` mux.HandleFunc("/v2/registry", func(w http.ResponseWriter, r *http.Request) { v := new(RegistryCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, v, createRequest) fmt.Fprint(w, createResponseJSON) }) got, _, err := client.Registry.Create(ctx, createRequest) require.NoError(t, err) require.Equal(t, want, got) } func TestRegistry_Get(t *testing.T) { setup() defer teardown() want := &Registry{ Name: testRegistry, } getResponseJSON := ` { "registry": { "name": "` + testRegistry + `" } }` mux.HandleFunc("/v2/registry", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, getResponseJSON) }) got, _, err := client.Registry.Get(ctx) require.NoError(t, err) require.Equal(t, want, got) } func TestRegistry_Delete(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/registry", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Registry.Delete(ctx) require.NoError(t, err) } func TestRegistry_DockerCredentials(t *testing.T) { returnedConfig := "this could be a docker config" tests := []struct { name string params *RegistryDockerCredentialsRequest expectedReadWrite string expectedExpirySeconds string }{ { name: "read-only (default)", params: &RegistryDockerCredentialsRequest{}, expectedReadWrite: "false", }, { name: "read/write", params: &RegistryDockerCredentialsRequest{ReadWrite: true}, expectedReadWrite: "true", }, { name: "read-only + custom expiry", params: &RegistryDockerCredentialsRequest{ExpirySeconds: intPtr(60 * 60)}, expectedReadWrite: "false", expectedExpirySeconds: "3600", }, { name: "read/write + custom expiry", params: &RegistryDockerCredentialsRequest{ReadWrite: true, ExpirySeconds: intPtr(60 * 60)}, expectedReadWrite: "true", expectedExpirySeconds: "3600", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/registry/docker-credentials", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, test.expectedReadWrite, r.URL.Query().Get("read_write")) require.Equal(t, test.expectedExpirySeconds, r.URL.Query().Get("expiry_seconds")) testMethod(t, r, http.MethodGet) fmt.Fprint(w, returnedConfig) }) got, _, err := client.Registry.DockerCredentials(ctx, test.params) require.NoError(t, err) require.Equal(t, []byte(returnedConfig), got.DockerConfigJSON) }) } } func TestRepository_List(t *testing.T) { setup() defer teardown() wantRepositories := []*Repository{ { RegistryName: testRegistry, Name: testRepository, TagCount: 1, LatestTag: &RepositoryTag{ RegistryName: testRegistry, Repository: testRepository, Tag: testTag, ManifestDigest: testDigest, CompressedSizeBytes: testCompressedSize, SizeBytes: testSize, UpdatedAt: testTime, }, }, } getResponseJSON := `{ "repositories": [ { "registry_name": "` + testRegistry + `", "name": "` + testRepository + `", "tag_count": 1, "latest_tag": { "registry_name": "` + testRegistry + `", "repository": "` + testRepository + `", "tag": "` + testTag + `", "manifest_digest": "` + testDigest + `", "compressed_size_bytes": ` + fmt.Sprintf("%d", testCompressedSize) + `, "size_bytes": ` + fmt.Sprintf("%d", testSize) + `, "updated_at": "` + testTimeString + `" } } ], "links": { "pages": { "next": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories?page=2", "last": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories?page=2" } }, "meta": { "total": 2 } }` mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories", testRegistry), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) testFormValues(t, r, map[string]string{"page": "1", "per_page": "1"}) fmt.Fprint(w, getResponseJSON) }) got, response, err := client.Registry.ListRepositories(ctx, testRegistry, &ListOptions{Page: 1, PerPage: 1}) require.NoError(t, err) require.Equal(t, wantRepositories, got) gotRespLinks := response.Links wantRespLinks := &Links{ Pages: &Pages{ Next: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories?page=2", testRegistry), Last: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories?page=2", testRegistry), }, } assert.Equal(t, wantRespLinks, gotRespLinks) gotRespMeta := response.Meta wantRespMeta := &Meta{ Total: 2, } assert.Equal(t, wantRespMeta, gotRespMeta) } func TestRepository_ListTags(t *testing.T) { setup() defer teardown() wantTags := []*RepositoryTag{ { RegistryName: testRegistry, Repository: testRepository, Tag: testTag, ManifestDigest: testDigest, CompressedSizeBytes: testCompressedSize, SizeBytes: testSize, UpdatedAt: testTime, }, } getResponseJSON := `{ "tags": [ { "registry_name": "` + testRegistry + `", "repository": "` + testRepository + `", "tag": "` + testTag + `", "manifest_digest": "` + testDigest + `", "compressed_size_bytes": ` + fmt.Sprintf("%d", testCompressedSize) + `, "size_bytes": ` + fmt.Sprintf("%d", testSize) + `, "updated_at": "` + testTimeString + `" } ], "links": { "pages": { "next": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories/` + testEncodedRepository + `/tags?page=2", "last": "https://api.digitalocean.com/v2/registry/` + testRegistry + `/repositories/` + testEncodedRepository + `/tags?page=2" } }, "meta": { "total": 2 } }` mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/tags", testRegistry, testRepository), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) testFormValues(t, r, map[string]string{"page": "1", "per_page": "1"}) fmt.Fprint(w, getResponseJSON) }) got, response, err := client.Registry.ListRepositoryTags(ctx, testRegistry, testRepository, &ListOptions{Page: 1, PerPage: 1}) require.NoError(t, err) require.Equal(t, wantTags, got) gotRespLinks := response.Links wantRespLinks := &Links{ Pages: &Pages{ Next: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories/%s/tags?page=2", testRegistry, testEncodedRepository), Last: fmt.Sprintf("https://api.digitalocean.com/v2/registry/%s/repositories/%s/tags?page=2", testRegistry, testEncodedRepository), }, } assert.Equal(t, wantRespLinks, gotRespLinks) gotRespMeta := response.Meta wantRespMeta := &Meta{ Total: 2, } assert.Equal(t, wantRespMeta, gotRespMeta) } func TestRegistry_DeleteTag(t *testing.T) { setup() defer teardown() mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/tags/%s", testRegistry, testRepository, testTag), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Registry.DeleteTag(ctx, testRegistry, testRepository, testTag) require.NoError(t, err) } func TestRegistry_DeleteManifest(t *testing.T) { setup() defer teardown() mux.HandleFunc(fmt.Sprintf("/v2/registry/%s/repositories/%s/digests/%s", testRegistry, testRepository, testDigest), func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Registry.DeleteManifest(ctx, testRegistry, testRepository, testDigest) require.NoError(t, err) } godo-1.37.0/sizes.go000066400000000000000000000033131366527000000142150ustar00rootroot00000000000000package godo import ( "context" "net/http" ) // 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"` Meta *Meta `json:"meta"` } // 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 } if m := root.Meta; m != nil { resp.Meta = m } return root.Sizes, resp, err } godo-1.37.0/sizes_test.go000066400000000000000000000047001366527000000152550ustar00rootroot00000000000000package godo import ( "fmt" "net/http" "reflect" "testing" ) func TestSizes_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{ "sizes": [ { "slug": "1" }, { "slug": "2" } ], "meta": { "total": 2 } }`) }) sizes, resp, err := client.Sizes.List(ctx, nil) if err != nil { t.Errorf("Sizes.List returned error: %v", err) } expectedSizes := []Size{{Slug: "1"}, {Slug: "2"}} if !reflect.DeepEqual(sizes, expectedSizes) { t.Errorf("Sizes.List returned sizes %+v, expected %+v", sizes, expectedSizes) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Sizes.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestSizes_ListSizesMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"sizes": [{"id":1},{"id":2}], "links":{"pages":{"next":"http://example.com/v2/sizes/?page=2"}}}`) }) _, resp, err := client.Sizes.List(ctx, nil) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestSizes_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "sizes": [{"id":1},{"id":2}], "links":{ "pages":{ "next":"http://example.com/v2/sizes/?page=3", "prev":"http://example.com/v2/sizes/?page=1", "last":"http://example.com/v2/sizes/?page=3", "first":"http://example.com/v2/sizes/?page=1" } } }` mux.HandleFunc("/v2/sizes", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) opt := &ListOptions{Page: 2} _, resp, err := client.Sizes.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestSize_String(t *testing.T) { size := &Size{ Slug: "slize", Memory: 123, Vcpus: 456, Disk: 789, PriceMonthly: 123, PriceHourly: 456, Regions: []string{"1", "2"}, Available: true, Transfer: 789, } stringified := size.String() expected := `godo.Size{Slug:"slize", Memory:123, Vcpus:456, Disk:789, PriceMonthly:123, PriceHourly:456, Regions:["1" "2"], Available:true, Transfer:789}` if expected != stringified { t.Errorf("Size.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/snapshots.go000066400000000000000000000100421366527000000150770ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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"` Tags []string `json:"tags,omitempty"` } type snapshotRoot struct { Snapshot *Snapshot `json:"snapshot"` } type snapshotsRoot struct { Snapshots []Snapshot `json:"snapshots"` Links *Links `json:"links,omitempty"` Meta *Meta `json:"meta,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 } godo-1.37.0/snapshots_test.go000066400000000000000000000121401366527000000161370ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" "reflect" "testing" ) func TestSnapshots_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`) }) ctx := context.Background() snapshots, _, err := client.Snapshots.List(ctx, nil) if err != nil { t.Errorf("Snapshots.List returned error: %v", err) } expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}} if !reflect.DeepEqual(snapshots, expected) { t.Errorf("Snapshots.List returned %+v, expected %+v", snapshots, expected) } } func TestSnapshots_ListVolume(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "volume" actual := r.URL.Query().Get("resource_type") if actual != expected { t.Errorf("'type' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2"}]}`) }) ctx := context.Background() snapshots, _, err := client.Snapshots.ListVolume(ctx, nil) if err != nil { t.Errorf("Snapshots.ListVolume returned error: %v", err) } expected := []Snapshot{{ID: "1"}, {ID: "2"}} if !reflect.DeepEqual(snapshots, expected) { t.Errorf("Snapshots.ListVolume returned %+v, expected %+v", snapshots, expected) } } func TestSnapshots_ListDroplet(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) expected := "droplet" actual := r.URL.Query().Get("resource_type") if actual != expected { t.Errorf("'resource_type' query = %v, expected %v", actual, expected) } fmt.Fprint(w, `{"snapshots":[{"id":"1"},{"id":"2", "size_gigabytes": 4.84}]}`) }) ctx := context.Background() snapshots, _, err := client.Snapshots.ListDroplet(ctx, nil) if err != nil { t.Errorf("Snapshots.ListDroplet returned error: %v", err) } expected := []Snapshot{{ID: "1"}, {ID: "2", SizeGigaBytes: 4.84}} if !reflect.DeepEqual(snapshots, expected) { t.Errorf("Snapshots.ListDroplet returned %+v, expected %+v", snapshots, expected) } } func TestSnapshots_ListSnapshotsMultiplePages(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"snapshots": [{"id":"1"},{"id":"2"}], "links":{"pages":{"next":"http://example.com/v2/snapshots/?page=2"}}}`) }) ctx := context.Background() _, resp, err := client.Snapshots.List(ctx, &ListOptions{Page: 2}) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 1) } func TestSnapshots_RetrievePageByNumber(t *testing.T) { setup() defer teardown() jBlob := ` { "snapshots": [{"id":"1"},{"id":"2"}], "links":{ "pages":{ "next":"http://example.com/v2/snapshots/?page=3", "prev":"http://example.com/v2/snapshots/?page=1", "last":"http://example.com/v2/snapshots/?page=3", "first":"http://example.com/v2/snapshots/?page=1" } } }` mux.HandleFunc("/v2/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) ctx := context.Background() opt := &ListOptions{Page: 2} _, resp, err := client.Snapshots.List(ctx, opt) if err != nil { t.Fatal(err) } checkCurrentPage(t, resp, 2) } func TestSnapshots_GetSnapshotByID(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, `{"snapshot":{"id":"12345"}}`) }) ctx := context.Background() snapshots, _, err := client.Snapshots.Get(ctx, "12345") if err != nil { t.Errorf("Snapshot.GetByID returned error: %v", err) } expected := &Snapshot{ID: "12345"} if !reflect.DeepEqual(snapshots, expected) { t.Errorf("Snapshots.GetByID returned %+v, expected %+v", snapshots, expected) } } func TestSnapshots_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots/12345", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) ctx := context.Background() _, err := client.Snapshots.Delete(ctx, "12345") if err != nil { t.Errorf("Snapshot.Delete returned error: %v", err) } } func TestSnapshot_String(t *testing.T) { snapshot := &Snapshot{ ID: "1", Name: "Snapsh176ot", ResourceID: "0", ResourceType: "droplet", Regions: []string{"one"}, MinDiskSize: 20, SizeGigaBytes: 4.84, Created: "2013-11-27T09:24:55Z", Tags: []string{"one", "two"}, } stringified := snapshot.String() expected := `godo.Snapshot{ID:"1", Name:"Snapsh176ot", ResourceID:"0", ResourceType:"droplet", Regions:["one"], MinDiskSize:20, SizeGigaBytes:4.84, Created:"2013-11-27T09:24:55Z", Tags:["one" "two"]}` if expected != stringified { t.Errorf("Snapshot.String returned %+v, expected %+v", stringified, expected) } } godo-1.37.0/storage.go000066400000000000000000000166141366527000000145340ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" "time" ) const ( storageBasePath = "v2" storageAllocPath = storageBasePath + "/volumes" storageSnapPath = storageBasePath + "/snapshots" ) // StorageService is an interface for interfacing with the storage // endpoints of the Digital Ocean API. // See: https://developers.digitalocean.com/documentation/v2/#block-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"` FilesystemType string `json:"filesystem_type"` FilesystemLabel string `json:"filesystem_label"` Tags []string `json:"tags"` } func (f Volume) String() string { return Stringify(f) } func (f Volume) URN() string { return ToURN("Volume", f.ID) } type storageVolumesRoot struct { Volumes []Volume `json:"volumes"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } 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"` FilesystemType string `json:"filesystem_type"` FilesystemLabel string `json:"filesystem_label"` Tags []string `json:"tags"` } // 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) } else if params.Region != "" { path = fmt.Sprintf("%s?region=%s", path, params.Region) } else if params.Name != "" { path = fmt.Sprintf("%s?name=%s", path, params.Name) } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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"` Tags []string `json:"tags"` } // 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 } if m := root.Meta; m != nil { resp.Meta = m } 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) } godo-1.37.0/storage_actions.go000066400000000000000000000100411366527000000162400ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) // 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 } if m := root.Meta; m != nil { resp.Meta = m } return root.Actions, resp, err } func storageAllocationActionPath(volumeID string) string { return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID) } godo-1.37.0/storage_actions_test.go000066400000000000000000000103531366527000000173050ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func TestStoragesActions_Attach(t *testing.T) { setup() defer teardown() const ( volumeID = "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" dropletID = 12345 ) attachRequest := &ActionRequest{ "type": "attach", "droplet_id": float64(dropletID), // encoding/json decodes numbers as floats } mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, attachRequest) { t.Errorf("want=%#v", attachRequest) t.Errorf("got=%#v", v) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) _, _, err := client.StorageActions.Attach(ctx, volumeID, dropletID) if err != nil { t.Errorf("StoragesActions.Attach returned error: %v", err) } } func TestStoragesActions_DetachByDropletID(t *testing.T) { setup() defer teardown() volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" dropletID := 123456 detachByDropletIDRequest := &ActionRequest{ "type": "detach", "droplet_id": float64(dropletID), // encoding/json decodes numbers as floats } mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, detachByDropletIDRequest) { t.Errorf("want=%#v", detachByDropletIDRequest) t.Errorf("got=%#v", v) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) _, _, err := client.StorageActions.DetachByDropletID(ctx, volumeID, dropletID) if err != nil { t.Errorf("StoragesActions.DetachByDropletID returned error: %v", err) } } func TestStorageActions_Get(t *testing.T) { setup() defer teardown() volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" mux.HandleFunc("/v2/volumes/"+volumeID+"/actions/456", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) action, _, err := client.StorageActions.Get(ctx, volumeID, 456) if err != nil { t.Errorf("StorageActions.Get returned error: %v", err) } expected := &Action{Status: "in-progress"} if !reflect.DeepEqual(action, expected) { t.Errorf("StorageActions.Get returned %+v, expected %+v", action, expected) } } func TestStorageActions_List(t *testing.T) { setup() defer teardown() volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprintf(w, `{ "actions": [ { "status": "in-progress" } ], "meta": { "total": 1 } }`) }) actions, resp, err := client.StorageActions.List(ctx, volumeID, nil) if err != nil { t.Errorf("StorageActions.List returned error: %v", err) } expectedActions := []Action{{Status: "in-progress"}} if !reflect.DeepEqual(actions, expectedActions) { t.Errorf("StorageActions.List returned actions %+v, expected %+v", actions, expectedActions) } expectedMeta := &Meta{Total: 1} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("StorageActions.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStoragesActions_Resize(t *testing.T) { setup() defer teardown() volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1" resizeRequest := &ActionRequest{ "type": "resize", "size_gigabytes": float64(500), "region": "nyc1", } mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) { v := new(ActionRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, resizeRequest) { t.Errorf("want=%#v", resizeRequest) t.Errorf("got=%#v", v) } fmt.Fprintf(w, `{"action":{"status":"in-progress"}}`) }) _, _, err := client.StorageActions.Resize(ctx, volumeID, 500, "nyc1") if err != nil { t.Errorf("StoragesActions.Resize returned error: %v", err) } } godo-1.37.0/storage_test.go000066400000000000000000000454761366527000000156030ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" "time" ) func TestStorageVolumes_ListStorageVolumes(t *testing.T) { setup() defer teardown() jBlob := ` { "volumes": [ { "user_id": 42, "region": {"slug": "nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my volume", "description": "my description", "size_gigabytes": 100, "droplet_ids": [10], "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "", "filesystem_label": "", "tags": ["tag1", "tag2"] }, { "user_id": 42, "region": {"slug": "nyc3"}, "id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", "name": "my other volume", "description": "my other description", "size_gigabytes": 100, "created_at": "2012-10-03T15:00:01.05Z", "filesystem_type": "ext4", "filesystem_label": "my-volume", "tags": [] } ], "links": { "pages": { "last": "https://api.digitalocean.com/v2/volumes?page=2", "next": "https://api.digitalocean.com/v2/volumes?page=2" } }, "meta": { "total": 28 } }` mux.HandleFunc("/v2/volumes/", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) volumes, resp, err := client.Storage.ListVolumes(ctx, nil) if err != nil { t.Errorf("Storage.ListVolumes returned error: %v", err) } expectedVolume := []Volume{ { Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my volume", Description: "my description", SizeGigaBytes: 100, DropletIDs: []int{10}, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, }, { Region: &Region{Slug: "nyc3"}, ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", Name: "my other volume", Description: "my other description", SizeGigaBytes: 100, CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC), FilesystemType: "ext4", FilesystemLabel: "my-volume", Tags: []string{}, }, } if !reflect.DeepEqual(volumes, expectedVolume) { t.Errorf("Storage.ListVolumes returned volumes %+v, expected %+v", volumes, expectedVolume) } expectedMeta := &Meta{ Total: 28, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Storage.ListVolumes returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStorageVolumes_Get(t *testing.T) { setup() defer teardown() want := &Volume{ Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my volume", Description: "my description", SizeGigaBytes: 100, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), FilesystemType: "xfs", FilesystemLabel: "my-vol", Tags: []string{"tag1", "tag2"}, } jBlob := `{ "volume":{ "region": {"slug":"nyc3"}, "attached_to_droplet": null, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my volume", "description": "my description", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "xfs", "filesystem_label": "my-vol", "tags": ["tag1", "tag2"] } }` mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.GetVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1") if err != nil { t.Errorf("Storage.GetVolume returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.GetVolume returned %+v, want %+v", got, want) } } func TestStorageVolumes_ListVolumesByName(t *testing.T) { setup() defer teardown() jBlob := `{ "volumes": [ { "region": {"slug": "nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "myvolume", "description": "my description", "size_gigabytes": 100, "droplet_ids": [10], "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "", "filesystem_label": "", "tags": ["tag1", "tag2"] } ], "links": {}, "meta": { "total": 1 } }` expectedVolumes := []Volume{ { Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "myvolume", Description: "my description", SizeGigaBytes: 100, DropletIDs: []int{10}, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, }, } mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("name") != "myvolume" || r.URL.Query().Get("region") != "" { t.Errorf("Storage.ListVolumeByName did not request the correct name or region") } testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) options := &ListVolumeParams{ Name: "myvolume", } volumes, resp, err := client.Storage.ListVolumes(ctx, options) if err != nil { t.Errorf("Storage.ListVolumeByName returned error: %v", err) } if !reflect.DeepEqual(volumes, expectedVolumes) { t.Errorf("Storage.ListVolumeByName returned volumes %+v, expected %+v", volumes, expectedVolumes) } expectedMeta := &Meta{ Total: 1, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Storage.ListVolumeByName returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStorageVolumes_ListVolumesByRegion(t *testing.T) { setup() defer teardown() jBlob := `{ "volumes": [ { "region": {"slug": "nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "myvolume", "description": "my description", "size_gigabytes": 100, "droplet_ids": [10], "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "", "filesystem_label": "", "tags": ["tag1", "tag2"] } ], "links": {}, "meta": { "total": 1 } }` expectedVolumes := []Volume{ { Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "myvolume", Description: "my description", SizeGigaBytes: 100, DropletIDs: []int{10}, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, }, } mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("region") != "nyc3" || r.URL.Query().Get("name") != "" { t.Errorf("Storage.ListVolumeByName did not request the correct name or region") } testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) options := &ListVolumeParams{ Region: "nyc3", } volumes, resp, err := client.Storage.ListVolumes(ctx, options) if err != nil { t.Errorf("Storage.ListVolumeByName returned error: %v", err) } if !reflect.DeepEqual(volumes, expectedVolumes) { t.Errorf("Storage.ListVolumeByName returned volumes %+v, expected %+v", volumes, expectedVolumes) } expectedMeta := &Meta{ Total: 1, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Storage.ListVolumeByName returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStorageVolumes_ListVolumesByNameAndRegion(t *testing.T) { setup() defer teardown() jBlob := `{ "volumes": [ { "region": {"slug": "nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "myvolume", "description": "my description", "size_gigabytes": 100, "droplet_ids": [10], "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "", "filesystem_label": "", "tags": ["tag1", "tag2"] } ], "links": {}, "meta": { "total": 1 } }` expectedVolumes := []Volume{ { Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "myvolume", Description: "my description", SizeGigaBytes: 100, DropletIDs: []int{10}, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, }, } mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("region") != "nyc3" || r.URL.Query().Get("name") != "myvolume" { t.Errorf("Storage.ListVolumeByName did not request the correct name or region") } testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) options := &ListVolumeParams{ Region: "nyc3", Name: "myvolume", } volumes, resp, err := client.Storage.ListVolumes(ctx, options) if err != nil { t.Errorf("Storage.ListVolumeByName returned error: %v", err) } if !reflect.DeepEqual(volumes, expectedVolumes) { t.Errorf("Storage.ListVolumeByName returned volumes %+v, expected %+v", volumes, expectedVolumes) } expectedMeta := &Meta{ Total: 1, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Storage.ListVolumeByName returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStorageVolumes_Create(t *testing.T) { setup() defer teardown() createRequest := &VolumeCreateRequest{ Region: "nyc3", Name: "my volume", Description: "my description", SizeGigaBytes: 100, Tags: []string{"tag1", "tag2"}, } want := &Volume{ Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my volume", Description: "my description", SizeGigaBytes: 100, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, } jBlob := `{ "volume":{ "region": {"slug":"nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my volume", "description": "my description", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z", "tags": ["tag1", "tag2"] }, "links": {} }` mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { v := new(VolumeCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.CreateVolume(ctx, createRequest) if err != nil { t.Errorf("Storage.CreateVolume returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want) } } func TestStorageVolumes_CreateFormatted(t *testing.T) { setup() defer teardown() createRequest := &VolumeCreateRequest{ Region: "nyc3", Name: "my volume", Description: "my description", SizeGigaBytes: 100, FilesystemType: "xfs", Tags: []string{"tag1", "tag2"}, } want := &Volume{ Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my volume", Description: "my description", SizeGigaBytes: 100, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), FilesystemType: "xfs", Tags: []string{"tag1", "tag2"}, } jBlob := `{ "volume":{ "region": {"slug":"nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my volume", "description": "my description", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z", "filesystem_type": "xfs", "filesystem_label": "", "tags": ["tag1", "tag2"] }, "links": {} }` mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { v := new(VolumeCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.CreateVolume(ctx, createRequest) if err != nil { t.Errorf("Storage.CreateVolume returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want) } } func TestStorageVolumes_CreateFromSnapshot(t *testing.T) { setup() defer teardown() createRequest := &VolumeCreateRequest{ Name: "my-volume-from-a-snapshot", Description: "my description", SizeGigaBytes: 100, SnapshotID: "0d165eff-0b4c-11e7-9093-0242ac110207", Tags: []string{"tag1", "tag2"}, } want := &Volume{ Region: &Region{Slug: "nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my-volume-from-a-snapshot", Description: "my description", SizeGigaBytes: 100, CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC), Tags: []string{"tag1", "tag2"}, } jBlob := `{ "volume":{ "region": {"slug":"nyc3"}, "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my-volume-from-a-snapshot", "description": "my description", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z", "tags": ["tag1", "tag2"] }, "links": {} }` mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) { v := new(VolumeCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.CreateVolume(ctx, createRequest) if err != nil { t.Errorf("Storage.CreateVolume returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.CreateVolume returned %+v, want %+v", got, want) } } func TestStorageVolumes_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Storage.DeleteVolume(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1") if err != nil { t.Errorf("Storage.DeleteVolume returned error: %v", err) } } func TestStorageSnapshots_ListStorageSnapshots(t *testing.T) { setup() defer teardown() jBlob := ` { "snapshots": [ { "regions": ["nyc3"], "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my snapshot", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z" }, { "regions": ["nyc3"], "id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", "name": "my other snapshot", "size_gigabytes": 100, "created_at": "2012-10-03T15:00:01.05Z" } ], "links": { "pages": { "last": "https://api.digitalocean.com/v2/volumes?page=2", "next": "https://api.digitalocean.com/v2/volumes?page=2" } }, "meta": { "total": 28 } }` mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) volumes, resp, err := client.Storage.ListSnapshots(ctx, "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", nil) if err != nil { t.Errorf("Storage.ListSnapshots returned error: %v", err) } expectedSnapshots := []Snapshot{ { Regions: []string{"nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my snapshot", SizeGigaBytes: 100, Created: "2002-10-02T15:00:00.05Z", }, { Regions: []string{"nyc3"}, ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1", Name: "my other snapshot", SizeGigaBytes: 100, Created: "2012-10-03T15:00:01.05Z", }, } if !reflect.DeepEqual(volumes, expectedSnapshots) { t.Errorf("Storage.ListSnapshots returned snapshots %+v, expected %+v", volumes, expectedSnapshots) } expectedMeta := &Meta{ Total: 28, } if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Storage.ListSnapshots returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestStorageSnapshots_Get(t *testing.T) { setup() defer teardown() want := &Snapshot{ Regions: []string{"nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my snapshot", SizeGigaBytes: 100, Created: "2002-10-02T15:00:00.05Z", } jBlob := `{ "snapshot":{ "regions": ["nyc3"], "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my snapshot", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z" } }` mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.GetSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1") if err != nil { t.Errorf("Storage.GetSnapshot returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.GetSnapshot returned %+v, want %+v", got, want) } } func TestStorageSnapshots_Create(t *testing.T) { setup() defer teardown() createRequest := &SnapshotCreateRequest{ VolumeID: "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my snapshot", Description: "my description", Tags: []string{"one", "two"}, } want := &Snapshot{ Regions: []string{"nyc3"}, ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", Name: "my snapshot", SizeGigaBytes: 100, Created: "2002-10-02T15:00:00.05Z", Tags: []string{"one", "two"}, } jBlob := `{ "snapshot":{ "regions": ["nyc3"], "id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1", "name": "my snapshot", "description": "my description", "size_gigabytes": 100, "created_at": "2002-10-02T15:00:00.05Z", "tags": ["one", "two"] }, "links": { "pages": { "last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2", "next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2" } }, "meta": { "total": 28 } }` mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) { v := new(SnapshotCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprint(w, jBlob) }) got, _, err := client.Storage.CreateSnapshot(ctx, createRequest) if err != nil { t.Errorf("Storage.CreateSnapshot returned error: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("Storage.CreateSnapshot returned %+v, want %+v", got, want) } } func TestStorageSnapshots_Destroy(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Storage.DeleteSnapshot(ctx, "80d414c6-295e-4e3a-ac58-eb9456c1e1d1") if err != nil { t.Errorf("Storage.DeleteSnapshot returned error: %v", err) } } godo-1.37.0/strings.go000066400000000000000000000037321366527000000145560ustar00rootroot00000000000000package godo import ( "bytes" "fmt" "io" "reflect" "strings" ) var timestampType = reflect.TypeOf(Timestamp{}) type ResourceWithURN interface { URN() string } // ToURN converts the resource type and ID to a valid DO API URN. func ToURN(resourceType string, id interface{}) string { return fmt.Sprintf("%s:%s:%v", "do", strings.ToLower(resourceType), id) } // 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{'}'}) } godo-1.37.0/tags.go000066400000000000000000000165051366527000000140250ustar00rootroot00000000000000package godo import ( "context" "fmt" "net/http" ) 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" // ImageResourceType holds the string representing our ResourceType of Image. ImageResourceType ResourceType = "image" // VolumeResourceType holds the string representing our ResourceType of Volume. VolumeResourceType ResourceType = "volume" // LoadBalancerResourceType holds the string representing our ResourceType of LoadBalancer. LoadBalancerResourceType ResourceType = "load_balancer" // VolumeSnapshotResourceType holds the string representing our ResourceType for storage Snapshots. VolumeSnapshotResourceType ResourceType = "volume_snapshot" // DatabaseResourceType holds the string representing our ResourceType of Database. DatabaseResourceType ResourceType = "database" ) // Resource represent a single resource for associating/disassociating with tags type Resource struct { ID string `json:"resource_id,omitempty"` Type ResourceType `json:"resource_type,omitempty"` } // TaggedResources represent the set of resources a tag is attached to type TaggedResources struct { Count int `json:"count"` LastTaggedURI string `json:"last_tagged_uri,omitempty"` Droplets *TaggedDropletsResources `json:"droplets,omitempty"` Images *TaggedImagesResources `json:"images"` Volumes *TaggedVolumesResources `json:"volumes"` VolumeSnapshots *TaggedVolumeSnapshotsResources `json:"volume_snapshots"` Databases *TaggedDatabasesResources `json:"databases"` } // 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"` LastTaggedURI string `json:"last_tagged_uri,omitempty"` } // TaggedResourcesData represent the generic resources a tag is attached to type TaggedResourcesData struct { Count int `json:"count,float64,omitempty"` LastTaggedURI string `json:"last_tagged_uri,omitempty"` } // TaggedImagesResources represent the image resources a tag is attached to type TaggedImagesResources TaggedResourcesData // TaggedVolumesResources represent the volume resources a tag is attached to type TaggedVolumesResources TaggedResourcesData // TaggedVolumeSnapshotsResources represent the volume snapshot resources a tag is attached to type TaggedVolumeSnapshotsResources TaggedResourcesData // TaggedDatabasesResources represent the database resources a tag is attached to type TaggedDatabasesResources TaggedResourcesData // 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"` Meta *Meta `json:"meta"` } 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 } if m := root.Meta; m != nil { resp.Meta = m } 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 } godo-1.37.0/tags_test.go000066400000000000000000000276221366527000000150660ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) var ( listEmptyJSON = ` { "tags": [ ], "meta": { "total": 0 } } ` listJSON = ` { "tags": [ { "name": "testing-1", "resources": { "count": 0, "droplets": { "count": 0, "last_tagged": null }, "images": { "count": 0 }, "volumes": { "count": 0 }, "volume_snapshots": { "count": 0 }, "databases": { "count": 0 } } }, { "name": "testing-2", "resources": { "count": 0, "droplets": { "count": 0, "last_tagged": null }, "images": { "count": 0 }, "volumes": { "count": 0 }, "volume_snapshots": { "count": 0 }, "databases": { "count": 0 } } } ], "links": { "pages":{ "next":"http://example.com/v2/tags/?page=3", "prev":"http://example.com/v2/tags/?page=1", "last":"http://example.com/v2/tags/?page=3", "first":"http://example.com/v2/tags/?page=1" } }, "meta": { "total": 2 } } ` createJSON = ` { "tag": { "name": "testing-1", "resources": { "count": 0, "droplets": { "count": 0, "last_tagged": null }, "images": { "count": 0 }, "volumes": { "count": 0 }, "volume_snapshots": { "count": 0 }, "databases": { "count": 0 } } } } ` getJSON = ` { "tag": { "name": "testing-1", "resources": { "count": 5, "last_tagged_uri": "https://api.digitalocean.com/v2/droplets/1", "droplets": { "count": 1, "last_tagged_uri": "https://api.digitalocean.com/v2/droplets/1", "last_tagged": { "id": 1, "name": "test.example.com", "memory": 1024, "vcpus": 2, "disk": 20, "region": { "slug": "nyc1", "name": "New York", "sizes": [ "1024mb", "512mb" ], "available": true, "features": [ "virtio", "private_networking", "backups", "ipv6" ] }, "image": { "id": 119192817, "name": "Ubuntu 13.04", "distribution": "ubuntu", "slug": "ubuntu1304", "public": true, "regions": [ "nyc1" ], "created_at": "2014-07-29T14:35:37Z" }, "size_slug": "1024mb", "locked": false, "status": "active", "networks": { "v4": [ { "ip_address": "10.0.0.19", "netmask": "255.255.0.0", "gateway": "10.0.0.1", "type": "private" }, { "ip_address": "127.0.0.19", "netmask": "255.255.255.0", "gateway": "127.0.0.20", "type": "public" } ], "v6": [ { "ip_address": "2001::13", "cidr": 124, "gateway": "2400:6180:0000:00D0:0000:0000:0009:7000", "type": "public" } ] }, "kernel": { "id": 485432985, "name": "DO-recovery-static-fsck", "version": "3.8.0-25-generic" }, "created_at": "2014-07-29T14:35:37Z", "features": [ "ipv6" ], "backup_ids": [ 449676382 ], "snapshot_ids": [ 449676383 ], "action_ids": [ ], "tags": [ "tag-1", "tag-2" ] } }, "images": { "count": 1, "last_tagged_uri": "https://api.digitalocean.com/v2/images/1" }, "volumes": { "count": 1, "last_tagged_uri": "https://api.digitalocean.com/v2/volumes/abc" }, "volume_snapshots": { "count": 1, "last_tagged_uri": "https://api.digitalocean.com/v2/snapshots/1" }, "databases": { "count": 1, "last_tagged_uri": "https://api.digitalocean.com/v2/databases/1" } } } } ` ) func TestTags_List(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, listJSON) }) tags, resp, err := client.Tags.List(ctx, nil) if err != nil { t.Errorf("Tags.List returned error: %v", err) } expectedTags := []Tag{ {Name: "testing-1", Resources: &TaggedResources{Count: 0, Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}, Images: &TaggedImagesResources{Count: 0}, Volumes: &TaggedVolumesResources{Count: 0}, VolumeSnapshots: &TaggedVolumeSnapshotsResources{Count: 0}, Databases: &TaggedDatabasesResources{Count: 0}}}, {Name: "testing-2", Resources: &TaggedResources{Count: 0, Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}, Images: &TaggedImagesResources{Count: 0}, Volumes: &TaggedVolumesResources{Count: 0}, VolumeSnapshots: &TaggedVolumeSnapshotsResources{Count: 0}, Databases: &TaggedDatabasesResources{Count: 0}}}, } if !reflect.DeepEqual(tags, expectedTags) { t.Errorf("Tags.List returned tags %+v, expected %+v", tags, expectedTags) } expectedMeta := &Meta{Total: 2} if !reflect.DeepEqual(resp.Meta, expectedMeta) { t.Errorf("Tags.List returned meta %+v, expected %+v", resp.Meta, expectedMeta) } } func TestTags_ListEmpty(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, listEmptyJSON) }) tags, _, err := client.Tags.List(ctx, nil) if err != nil { t.Errorf("Tags.List returned error: %v", err) } expected := []Tag{} if !reflect.DeepEqual(tags, expected) { t.Errorf("Tags.List returned %+v, expected %+v", tags, expected) } } func TestTags_ListPaging(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, listJSON) }) _, resp, err := client.Tags.List(ctx, nil) if err != nil { t.Errorf("Tags.List returned error: %v", err) } checkCurrentPage(t, resp, 2) } func TestTags_Get(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, getJSON) }) tag, _, err := client.Tags.Get(ctx, "testing-1") if err != nil { t.Errorf("Tags.Get returned error: %v", err) } if tag.Name != "testing-1" { t.Errorf("Tags.Get return an incorrect name, got %+v, expected %+v", tag.Name, "testing-1") } if tag.Resources.Count != 5 { t.Errorf("Tags.Get return an incorrect resource count, got %+v, expected %+v", tag.Resources.Count, 5) } if tag.Resources.LastTaggedURI != "https://api.digitalocean.com/v2/droplets/1" { t.Errorf("Tags.Get return an incorrect last tagged uri %+v, expected %+v", tag.Resources.LastTaggedURI, "https://api.digitalocean.com/v2/droplets/1") } if tag.Resources.Droplets.Count != 1 { t.Errorf("Tags.Get return an incorrect droplet resource count, got %+v, expected %+v", tag.Resources.Droplets.Count, 1) } if tag.Resources.Droplets.LastTagged.ID != 1 { t.Errorf("Tags.Get return an incorrect last tagged droplet %+v, expected %+v", tag.Resources.Droplets.LastTagged.ID, 1) } if tag.Resources.Droplets.LastTaggedURI != "https://api.digitalocean.com/v2/droplets/1" { t.Errorf("Tags.Get return an incorrect last tagged droplet uri %+v, expected %+v", tag.Resources.Droplets.LastTaggedURI, "https://api.digitalocean.com/v2/droplets/1") } if tag.Resources.Images.Count != 1 { t.Errorf("Tags.Get return an incorrect image resource count, got %+v, expected %+v", tag.Resources.Images.Count, 1) } if tag.Resources.Images.LastTaggedURI != "https://api.digitalocean.com/v2/images/1" { t.Errorf("Tags.Get return an incorrect last tagged droplet uri %+v, expected %+v", tag.Resources.Images.LastTaggedURI, "https://api.digitalocean.com/v2/images/1") } if tag.Resources.Volumes.Count != 1 { t.Errorf("Tags.Get return an incorrect volume resource count, got %+v, expected %+v", tag.Resources.Volumes.Count, 1) } if tag.Resources.Volumes.LastTaggedURI != "https://api.digitalocean.com/v2/volumes/abc" { t.Errorf("Tags.Get return an incorrect last tagged volume uri %+v, expected %+v", tag.Resources.Volumes.LastTaggedURI, "https://api.digitalocean.com/v2/volumes/abc") } if tag.Resources.VolumeSnapshots.Count != 1 { t.Errorf("Tags.Get return an incorrect volume snapshot resource count, got %+v, expected %+v", tag.Resources.VolumeSnapshots.Count, 1) } if tag.Resources.VolumeSnapshots.LastTaggedURI != "https://api.digitalocean.com/v2/snapshots/1" { t.Errorf("Tags.Get return an incorrect last tagged volume snapshot uri %+v, expected %+v", tag.Resources.VolumeSnapshots.LastTaggedURI, "https://api.digitalocean.com/v2/snapshots/1") } if tag.Resources.Databases.Count != 1 { t.Errorf("Tags.Get return an incorrect database resource count, got %+v, expected %+v", tag.Resources.Databases.Count, 1) } if tag.Resources.Databases.LastTaggedURI != "https://api.digitalocean.com/v2/databases/1" { t.Errorf("Tags.Get return an incorrect last tagged database uri %+v, expected %+v", tag.Resources.Databases.LastTaggedURI, "https://api.digitalocean.com/v2/databases/1") } } func TestTags_Create(t *testing.T) { setup() defer teardown() createRequest := &TagCreateRequest{ Name: "testing-1", } mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) { v := new(TagCreateRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, createRequest) { t.Errorf("Request body = %+v, expected %+v", v, createRequest) } fmt.Fprintf(w, createJSON) }) tag, _, err := client.Tags.Create(ctx, createRequest) if err != nil { t.Errorf("Tags.Create returned error: %v", err) } expected := &Tag{ Name: "testing-1", Resources: &TaggedResources{ Count: 0, Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}, Images: &TaggedImagesResources{Count: 0}, Volumes: &TaggedVolumesResources{Count: 0}, VolumeSnapshots: &TaggedVolumeSnapshotsResources{Count: 0}, Databases: &TaggedDatabasesResources{Count: 0}, }, } if !reflect.DeepEqual(tag, expected) { t.Errorf("Tags.Create returned %+v, expected %+v", tag, expected) } } func TestTags_Delete(t *testing.T) { setup() defer teardown() mux.HandleFunc("/v2/tags/testing-1", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := client.Tags.Delete(ctx, "testing-1") if err != nil { t.Errorf("Tags.Delete returned error: %v", err) } } func TestTags_TagResource(t *testing.T) { setup() defer teardown() tagResourcesRequest := &TagResourcesRequest{ Resources: []Resource{ {ID: "1", Type: DropletResourceType}, {ID: "1", Type: ImageResourceType}, }, } mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) { v := new(TagResourcesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodPost) if !reflect.DeepEqual(v, tagResourcesRequest) { t.Errorf("Request body = %+v, expected %+v", v, tagResourcesRequest) } }) _, err := client.Tags.TagResources(ctx, "testing-1", tagResourcesRequest) if err != nil { t.Errorf("Tags.TagResources returned error: %v", err) } } func TestTags_UntagResource(t *testing.T) { setup() defer teardown() untagResourcesRequest := &UntagResourcesRequest{ Resources: []Resource{ {ID: "1", Type: DropletResourceType}, {ID: "1", Type: ImageResourceType}, }, } mux.HandleFunc("/v2/tags/testing-1/resources", func(w http.ResponseWriter, r *http.Request) { v := new(UntagResourcesRequest) err := json.NewDecoder(r.Body).Decode(v) if err != nil { t.Fatalf("decode json: %v", err) } testMethod(t, r, http.MethodDelete) if !reflect.DeepEqual(v, untagResourcesRequest) { t.Errorf("Request body = %+v, expected %+v", v, untagResourcesRequest) } }) _, err := client.Tags.UntagResources(ctx, "testing-1", untagResourcesRequest) if err != nil { t.Errorf("Tags.UntagResources returned error: %v", err) } } godo-1.37.0/timestamp.go000066400000000000000000000014751366527000000150720ustar00rootroot00000000000000package 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) } godo-1.37.0/timestamp_test.go000066400000000000000000000123601366527000000161240ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "testing" "time" ) const ( emptyTimeStr = `"0001-01-01T00:00:00Z"` referenceTimeStr = `"2006-01-02T15:04:05Z"` referenceUnixTimeStr = `1136214245` ) var ( referenceTime = time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC) unixOrigin = time.Unix(0, 0).In(time.UTC) ) func TestTimestamp_Marshal(t *testing.T) { testCases := []struct { desc string data Timestamp want string wantErr bool equal bool }{ {"Reference", Timestamp{referenceTime}, referenceTimeStr, false, true}, {"Empty", Timestamp{}, emptyTimeStr, false, true}, {"Mismatch", Timestamp{}, referenceTimeStr, false, false}, } for _, tc := range testCases { out, err := json.Marshal(tc.data) if gotErr := (err != nil); gotErr != tc.wantErr { t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) } got := string(out) equal := got == tc.want if (got == tc.want) != tc.equal { t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) } } } func TestTimestamp_Unmarshal(t *testing.T) { testCases := []struct { desc string data string want Timestamp wantErr bool equal bool }{ {"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true}, {"ReferenceUnix", `1136214245`, Timestamp{referenceTime}, false, true}, {"Empty", emptyTimeStr, Timestamp{}, false, true}, {"UnixStart", `0`, Timestamp{unixOrigin}, false, true}, {"Mismatch", referenceTimeStr, Timestamp{}, false, false}, {"MismatchUnix", `0`, Timestamp{}, false, false}, {"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false}, } for _, tc := range testCases { var got Timestamp err := json.Unmarshal([]byte(tc.data), &got) if gotErr := err != nil; gotErr != tc.wantErr { t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) continue } equal := got.Equal(tc.want) if equal != tc.equal { t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) } } } func TestTimstamp_MarshalReflexivity(t *testing.T) { testCases := []struct { desc string data Timestamp }{ {"Reference", Timestamp{referenceTime}}, {"Empty", Timestamp{}}, } for _, tc := range testCases { data, err := json.Marshal(tc.data) if err != nil { t.Errorf("%s: Marshal err=%v", tc.desc, err) } var got Timestamp err = json.Unmarshal(data, &got) if err != nil { t.Errorf("%s: Unmarshal err=%v", data, err) } if !got.Equal(tc.data) { t.Errorf("%s: %+v != %+v", tc.desc, got, data) } } } type WrappedTimestamp struct { A int Time Timestamp } func TestWrappedTimstamp_Marshal(t *testing.T) { testCases := []struct { desc string data WrappedTimestamp want string wantErr bool equal bool }{ {"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, true}, {"Empty", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, emptyTimeStr), false, true}, {"Mismatch", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, false}, } for _, tc := range testCases { out, err := json.Marshal(tc.data) if gotErr := err != nil; gotErr != tc.wantErr { t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) } got := string(out) equal := got == tc.want if equal != tc.equal { t.Errorf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) } } } func TestWrappedTimestamp_Unmarshal(t *testing.T) { testCases := []struct { desc string data string want WrappedTimestamp wantErr bool equal bool }{ {"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true}, {"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true}, {"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true}, {"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true}, {"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false}, {"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false}, {"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false}, } for _, tc := range testCases { var got Timestamp err := json.Unmarshal([]byte(tc.data), &got) if gotErr := err != nil; gotErr != tc.wantErr { t.Errorf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err) continue } equal := got.Time.Equal(tc.want.Time.Time) if equal != tc.equal { t.Errorf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal) } } } func TestWrappedTimestamp_MarshalReflexivity(t *testing.T) { testCases := []struct { desc string data WrappedTimestamp }{ {"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}}, {"Empty", WrappedTimestamp{0, Timestamp{}}}, } for _, tc := range testCases { bytes, err := json.Marshal(tc.data) if err != nil { t.Errorf("%s: Marshal err=%v", tc.desc, err) } var got WrappedTimestamp err = json.Unmarshal(bytes, &got) if err != nil { t.Errorf("%s: Unmarshal err=%v", bytes, err) } if !got.Time.Equal(tc.data.Time) { t.Errorf("%s: %+v != %+v", tc.desc, got, tc.data) } } } godo-1.37.0/util/000077500000000000000000000000001366527000000135065ustar00rootroot00000000000000godo-1.37.0/util/droplet.go000066400000000000000000000021121366527000000155020ustar00rootroot00000000000000package util import ( "context" "fmt" "time" "github.com/digitalocean/godo" ) const ( // activeFailure is the amount of times we can fail before deciding // the check for active is a total failure. This can help account // for servers randomly not answering. activeFailure = 3 ) // WaitForActive waits for a droplet to become active func WaitForActive(ctx context.Context, client *godo.Client, monitorURI string) error { if len(monitorURI) == 0 { return fmt.Errorf("create had no monitor uri") } completed := false failCount := 0 for !completed { action, _, err := client.DropletActions.GetByURI(ctx, monitorURI) if err != nil { select { case <-ctx.Done(): return err default: } if failCount <= activeFailure { failCount++ continue } return err } switch action.Status { case godo.ActionInProgress: select { case <-time.After(5 * time.Second): case <-ctx.Done(): return err } case godo.ActionCompleted: completed = true default: return fmt.Errorf("unknown status: [%s]", action.Status) } } return nil } godo-1.37.0/util/droplet_test.go000066400000000000000000000011071366527000000165440ustar00rootroot00000000000000package util import ( "context" "golang.org/x/oauth2" "github.com/digitalocean/godo" ) func ExampleWaitForActive() { // build client pat := "mytoken" token := &oauth2.Token{AccessToken: pat} t := oauth2.StaticTokenSource(token) ctx := context.TODO() oauthClient := oauth2.NewClient(ctx, t) client := godo.NewClient(oauthClient) // create your droplet and retrieve the create action uri uri := "https://api.digitalocean.com/v2/actions/xxxxxxxx" // block until until the action is complete err := WaitForActive(ctx, client, uri) if err != nil { panic(err) } } godo-1.37.0/vpcs.go000066400000000000000000000126511366527000000140400ustar00rootroot00000000000000package godo import ( "context" "net/http" "time" ) const vpcsBasePath = "/v2/vpcs" // VPCsService is an interface for managing Virtual Private Cloud configurations with the // DigitalOcean API. // See: https://developers.digitalocean.com/documentation/v2#vpcs type VPCsService interface { Create(context.Context, *VPCCreateRequest) (*VPC, *Response, error) Get(context.Context, string) (*VPC, *Response, error) List(context.Context, *ListOptions) ([]*VPC, *Response, error) Update(context.Context, string, *VPCUpdateRequest) (*VPC, *Response, error) Set(context.Context, string, ...VPCSetField) (*VPC, *Response, error) Delete(context.Context, string) (*Response, error) } var _ VPCsService = &VPCsServiceOp{} // VPCsServiceOp interfaces with VPC endpoints in the DigitalOcean API. type VPCsServiceOp struct { client *Client } // VPCCreateRequest represents a request to create a Virtual Private Cloud. type VPCCreateRequest struct { Name string `json:"name,omitempty"` RegionSlug string `json:"region,omitempty"` Description string `json:"description,omitempty"` IPRange string `json:"ip_range,omitempty"` } // VPCUpdateRequest represents a request to update a Virtual Private Cloud. type VPCUpdateRequest struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` } // VPCSetField allows one to set individual fields within a VPC configuration. type VPCSetField interface { vpcSetField(map[string]interface{}) } // VPCSetName is used when one want to set the `name` field of a VPC. // Ex.: VPCs.Set(..., VPCSetName("new-name")) type VPCSetName string // VPCSetDescription is used when one want to set the `description` field of a VPC. // Ex.: VPCs.Set(..., VPCSetDescription("vpc description")) type VPCSetDescription string // VPC represents a DigitalOcean Virtual Private Cloud configuration. type VPC struct { ID string `json:"id,omitempty"` URN string `json:"urn"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` IPRange string `json:"ip_range,omitempty"` RegionSlug string `json:"region,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` Default bool `json:"default,omitempty"` } type vpcRoot struct { VPC *VPC `json:"vpc"` } type vpcsRoot struct { VPCs []*VPC `json:"vpcs"` Links *Links `json:"links"` Meta *Meta `json:"meta"` } // Get returns the details of a Virtual Private Cloud. func (v *VPCsServiceOp) Get(ctx context.Context, id string) (*VPC, *Response, error) { path := vpcsBasePath + "/" + id req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(vpcRoot) resp, err := v.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.VPC, resp, nil } // Create creates a new Virtual Private Cloud. func (v *VPCsServiceOp) Create(ctx context.Context, create *VPCCreateRequest) (*VPC, *Response, error) { path := vpcsBasePath req, err := v.client.NewRequest(ctx, http.MethodPost, path, create) if err != nil { return nil, nil, err } root := new(vpcRoot) resp, err := v.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.VPC, resp, nil } // List returns a list of the caller's VPCs, with optional pagination. func (v *VPCsServiceOp) List(ctx context.Context, opt *ListOptions) ([]*VPC, *Response, error) { path, err := addOptions(vpcsBasePath, opt) if err != nil { return nil, nil, err } req, err := v.client.NewRequest(ctx, http.MethodGet, path, nil) if err != nil { return nil, nil, err } root := new(vpcsRoot) resp, err := v.client.Do(ctx, req, root) if err != nil { return nil, resp, err } if l := root.Links; l != nil { resp.Links = l } if m := root.Meta; m != nil { resp.Meta = m } return root.VPCs, resp, nil } // Update updates a Virtual Private Cloud's properties. func (v *VPCsServiceOp) Update(ctx context.Context, id string, update *VPCUpdateRequest) (*VPC, *Response, error) { path := vpcsBasePath + "/" + id req, err := v.client.NewRequest(ctx, http.MethodPut, path, update) if err != nil { return nil, nil, err } root := new(vpcRoot) resp, err := v.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.VPC, resp, nil } func (n VPCSetName) vpcSetField(in map[string]interface{}) { in["name"] = n } func (n VPCSetDescription) vpcSetField(in map[string]interface{}) { in["description"] = n } // Set updates specific properties of a Virtual Private Cloud. func (v *VPCsServiceOp) Set(ctx context.Context, id string, fields ...VPCSetField) (*VPC, *Response, error) { path := vpcsBasePath + "/" + id update := make(map[string]interface{}, len(fields)) for _, field := range fields { field.vpcSetField(update) } req, err := v.client.NewRequest(ctx, http.MethodPatch, path, update) if err != nil { return nil, nil, err } root := new(vpcRoot) resp, err := v.client.Do(ctx, req, root) if err != nil { return nil, resp, err } return root.VPC, resp, nil } // Delete deletes a Virtual Private Cloud. There is no way to recover a VPC once it has been // destroyed. func (v *VPCsServiceOp) Delete(ctx context.Context, id string) (*Response, error) { path := vpcsBasePath + "/" + id req, err := v.client.NewRequest(ctx, http.MethodDelete, path, nil) if err != nil { return nil, err } resp, err := v.client.Do(ctx, req, nil) if err != nil { return resp, err } return resp, nil } godo-1.37.0/vpcs_test.go000066400000000000000000000110261366527000000150720ustar00rootroot00000000000000package godo import ( "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var vTestObj = &VPC{ ID: "880b7f98-f062-404d-b33c-458d545696f6", URN: "do:vpc:880b7f98-f062-404d-b33c-458d545696f6", Name: "my-new-vpc", Description: "vpc description", IPRange: "10.122.0.0/20", RegionSlug: "s2r7", CreatedAt: time.Date(2019, 2, 4, 21, 48, 40, 995304079, time.UTC), Default: false, } var vTestJSON = ` { "id":"880b7f98-f062-404d-b33c-458d545696f6", "urn":"do:vpc:880b7f98-f062-404d-b33c-458d545696f6", "name":"my-new-vpc", "description":"vpc description", "ip_range":"10.122.0.0/20", "region":"s2r7", "created_at":"2019-02-04T21:48:40.995304079Z", "default":false } ` func TestVPCs_Get(t *testing.T) { setup() defer teardown() svc := client.VPCs path := "/v2/vpcs" want := vTestObj id := "880b7f98-f062-404d-b33c-458d545696f6" jsonBlob := ` { "vpc": ` + vTestJSON + ` } ` mux.HandleFunc(path+"/"+id, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jsonBlob) }) got, _, err := svc.Get(ctx, id) require.NoError(t, err) require.Equal(t, want, got) } func TestVPCs_List(t *testing.T) { setup() defer teardown() svc := client.VPCs path := "/v2/vpcs" want := []*VPC{ vTestObj, } links := &Links{ Pages: &Pages{ Last: "http://localhost/v2/vpcs?page=3&per_page=1", Next: "http://localhost/v2/vpcs?page=2&per_page=1", }, } meta := &Meta{ Total: 3, } jsonBlob := ` { "vpcs": [ ` + vTestJSON + ` ], "links": { "pages": { "last": "http://localhost/v2/vpcs?page=3&per_page=1", "next": "http://localhost/v2/vpcs?page=2&per_page=1" } }, "meta": {"total": 3} } ` mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) fmt.Fprint(w, jsonBlob) }) got, resp, err := svc.List(ctx, nil) require.NoError(t, err) assert.Equal(t, want, got) assert.Equal(t, resp.Links, links) assert.Equal(t, resp.Meta, meta) } func TestVPCs_Create(t *testing.T) { setup() defer teardown() svc := client.VPCs path := "/v2/vpcs" want := vTestObj req := &VPCCreateRequest{ Name: "my-new-vpc", RegionSlug: "s2r7", } jsonBlob := ` { "vpc": ` + vTestJSON + ` } ` mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { c := new(VPCCreateRequest) err := json.NewDecoder(r.Body).Decode(c) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPost) require.Equal(t, c, req) fmt.Fprint(w, jsonBlob) }) got, _, err := svc.Create(ctx, req) require.NoError(t, err) require.Equal(t, want, got) } func TestVPCs_Update(t *testing.T) { setup() defer teardown() svc := client.VPCs path := "/v2/vpcs" want := vTestObj id := "880b7f98-f062-404d-b33c-458d545696f6" req := &VPCUpdateRequest{ Name: "my-new-vpc", Description: "vpc description", } jsonBlob := ` { "vpc": ` + vTestJSON + ` } ` mux.HandleFunc(path+"/"+id, func(w http.ResponseWriter, r *http.Request) { c := new(VPCUpdateRequest) err := json.NewDecoder(r.Body).Decode(c) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPut) require.Equal(t, c, req) fmt.Fprint(w, jsonBlob) }) got, _, err := svc.Update(ctx, id, req) require.NoError(t, err) require.Equal(t, want, got) } func TestVPCs_Set(t *testing.T) { setup() defer teardown() type setRequest struct { Name string `json:"name"` Description string `json:"description"` } svc := client.VPCs path := "/v2/vpcs" want := vTestObj id := "880b7f98-f062-404d-b33c-458d545696f6" name := "my-new-vpc" desc := "vpc description" req := &setRequest{ Name: name, Description: desc, } jsonBlob := ` { "vpc": ` + vTestJSON + ` } ` mux.HandleFunc(path+"/"+id, func(w http.ResponseWriter, r *http.Request) { c := new(setRequest) err := json.NewDecoder(r.Body).Decode(c) if err != nil { t.Fatal(err) } testMethod(t, r, http.MethodPatch) require.Equal(t, c, req) fmt.Fprint(w, jsonBlob) }) got, _, err := svc.Set(ctx, id, VPCSetName(name), VPCSetDescription(desc)) require.NoError(t, err) require.Equal(t, want, got) } func TestVPCs_Delete(t *testing.T) { setup() defer teardown() svc := client.VPCs path := "/v2/vpcs" id := "880b7f98-f062-404d-b33c-458d545696f6" mux.HandleFunc(path+"/"+id, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodDelete) }) _, err := svc.Delete(ctx, id) require.NoError(t, err) }