mirror of https://github.com/kubernetes/kops.git
vendor github.com/digitalocean/godo
This commit is contained in:
parent
292e6971ad
commit
98a130a8a3
|
@ -313,3 +313,6 @@
|
|||
[submodule "_vendor/github.com/Azure/go-autorest"]
|
||||
path = _vendor/github.com/Azure/go-autorest
|
||||
url = https://github.com/Azure/go-autorest
|
||||
[submodule "_vendor/github.com/digitalocean/godo"]
|
||||
path = _vendor/github.com/digitalocean/godo
|
||||
url = https://github.com/digitalocean/godo.git
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 34840385860db94c88d044571153b6a200ca40b2
|
|
@ -0,0 +1 @@
|
|||
vendor/
|
|
@ -0,0 +1,6 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.6.3
|
||||
- 1.7
|
||||
- tip
|
|
@ -0,0 +1,20 @@
|
|||
# Change Log
|
||||
|
||||
## [v1.1.0] - 2017-06-06
|
||||
|
||||
### Added
|
||||
- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola
|
||||
- #139 Add TTL field to the Domains. - @xmudrii
|
||||
|
||||
### Fixed
|
||||
- #143 Fix oauth2.NoContext depreciation. - @jbowens
|
||||
- #141 Fix DropletActions on tagged resources. - @xmudrii
|
||||
|
||||
## [v1.0.0] - 2017-03-10
|
||||
|
||||
### Added
|
||||
- #130 Add Convert to ImageActionsService. - @xmudrii
|
||||
- #126 Add CertificatesService for managing certificates with the DigitalOcean API. - @viola
|
||||
- #125 Add LoadBalancersService for managing load balancers with the DigitalOcean API. - @viola
|
||||
- #122 Add GetVolumeByName to StorageService. - @protochron
|
||||
- #113 Add context.Context to all calls. - @aybabtme
|
|
@ -0,0 +1,23 @@
|
|||
# Contributing
|
||||
|
||||
If you submit a pull request, please keep the following guidelines in mind:
|
||||
|
||||
1. Code should be `go fmt` compliant.
|
||||
2. Types, structs and funcs should be documented.
|
||||
3. Tests pass.
|
||||
|
||||
## Getting set up
|
||||
|
||||
Assuming your `$GOPATH` is set up according to your desires, run:
|
||||
|
||||
```sh
|
||||
go get github.com/digitalocean/godo
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
When working on code in this repository, tests can be run via:
|
||||
|
||||
```sh
|
||||
go test .
|
||||
```
|
|
@ -0,0 +1,55 @@
|
|||
Copyright (c) 2014-2016 The godo AUTHORS. All rights reserved.
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
======================
|
||||
Portions of the client are based on code at:
|
||||
https://github.com/google/go-github/
|
||||
|
||||
Copyright (c) 2013 The go-github AUTHORS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
[](https://travis-ci.org/digitalocean/godo)
|
||||
|
||||
# Godo
|
||||
|
||||
Godo is a Go client library for accessing the DigitalOcean V2 API.
|
||||
|
||||
You can view the client API docs here: [http://godoc.org/github.com/digitalocean/godo](http://godoc.org/github.com/digitalocean/godo)
|
||||
|
||||
You can view DigitalOcean API docs here: [https://developers.digitalocean.com/documentation/v2/](https://developers.digitalocean.com/documentation/v2/)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/digitalocean/godo"
|
||||
```
|
||||
|
||||
Create a new DigitalOcean client, then use the exposed services to
|
||||
access different parts of the DigitalOcean API.
|
||||
|
||||
### Authentication
|
||||
|
||||
Currently, Personal Access Token (PAT) is the only method of
|
||||
authenticating with the API. You can manage your tokens
|
||||
at the DigitalOcean Control Panel [Applications Page](https://cloud.digitalocean.com/settings/applications).
|
||||
|
||||
You can then use your token to create a new client:
|
||||
|
||||
```go
|
||||
import "golang.org/x/oauth2"
|
||||
|
||||
pat := "mytoken"
|
||||
type TokenSource struct {
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
func (t *TokenSource) Token() (*oauth2.Token, error) {
|
||||
token := &oauth2.Token{
|
||||
AccessToken: t.AccessToken,
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
tokenSource := &TokenSource{
|
||||
AccessToken: pat,
|
||||
}
|
||||
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
|
||||
client := godo.NewClient(oauthClient)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
To create a new Droplet:
|
||||
|
||||
```go
|
||||
dropletName := "super-cool-droplet"
|
||||
|
||||
createRequest := &godo.DropletCreateRequest{
|
||||
Name: dropletName,
|
||||
Region: "nyc3",
|
||||
Size: "512mb",
|
||||
Image: godo.DropletCreateImage{
|
||||
Slug: "ubuntu-14-04-x64",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Something bad happened: %s\n\n", err)
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
If a list of items is paginated by the API, you must request pages individually. For example, to fetch all Droplets:
|
||||
|
||||
```go
|
||||
func DropletList(ctx context.Context, client *godo.Client) ([]godo.Droplet, error) {
|
||||
// create a list to hold our droplets
|
||||
list := []godo.Droplet{}
|
||||
|
||||
// create options. initially, these will be blank
|
||||
opt := &godo.ListOptions{}
|
||||
for {
|
||||
droplets, resp, err := client.Droplets.List(ctx, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// append the current page's droplets to our list
|
||||
for _, d := range droplets {
|
||||
list = append(list, d)
|
||||
}
|
||||
|
||||
// if we are at the last page, break out the for loop
|
||||
if resp.Links == nil || resp.Links.IsLastPage() {
|
||||
break
|
||||
}
|
||||
|
||||
page, err := resp.Links.CurrentPage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the page we want for the next request
|
||||
opt.Page = page + 1
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Each version of the client is tagged and the version is updated accordingly.
|
||||
|
||||
Since Go does not have a built-in versioning, a package management tool is
|
||||
recommended - a good one that works with git tags is
|
||||
[gopkg.in](http://labix.org/gopkg.in).
|
||||
|
||||
To see the list of past versions, run `git tag`.
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
For a comprehensive list of examples, check out the [API documentation](https://developers.digitalocean.com/documentation/v2/).
|
||||
|
||||
For details on all the functionality in this library, see the [GoDoc](http://godoc.org/github.com/digitalocean/godo) documentation.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
We love pull requests! Please see the [contribution guidelines](CONTRIBUTING.md).
|
|
@ -0,0 +1,60 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// AccountService is an interface for interfacing with the Account
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#account
|
||||
type AccountService interface {
|
||||
Get(context.Context) (*Account, *Response, error)
|
||||
}
|
||||
|
||||
// AccountServiceOp handles communication with the Account related methods of
|
||||
// the DigitalOcean API.
|
||||
type AccountServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ AccountService = &AccountServiceOp{}
|
||||
|
||||
// Account represents a DigitalOcean Account
|
||||
type Account struct {
|
||||
DropletLimit int `json:"droplet_limit,omitempty"`
|
||||
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
UUID string `json:"uuid,omitempty"`
|
||||
EmailVerified bool `json:"email_verified,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
StatusMessage string `json:"status_message,omitempty"`
|
||||
}
|
||||
|
||||
type accountRoot struct {
|
||||
Account *Account `json:"account"`
|
||||
}
|
||||
|
||||
func (r Account) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// Get DigitalOcean account info
|
||||
func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error) {
|
||||
|
||||
path := "v2/account"
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(accountRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Account, resp, err
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package 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,
|
||||
"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}
|
||||
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",
|
||||
}
|
||||
|
||||
stringified := acct.String()
|
||||
expected := `godo.Account{DropletLimit:25, FloatingIPLimit:25, Email:"sammy@digitalocean.com", UUID:"b6fr89dbf6d9156cace5f3c78dc9851d957381ef", EmailVerified:true, Status:"active", StatusMessage:"message"}`
|
||||
if expected != stringified {
|
||||
t.Errorf("Account.String returned %+v, expected %+v", stringified, expected)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const (
|
||||
actionsBasePath = "v2/actions"
|
||||
|
||||
// ActionInProgress is an in progress action status
|
||||
ActionInProgress = "in-progress"
|
||||
|
||||
//ActionCompleted is a completed action status
|
||||
ActionCompleted = "completed"
|
||||
)
|
||||
|
||||
// ActionsService handles communction with action related methods of the
|
||||
// DigitalOcean API: https://developers.digitalocean.com/documentation/v2#actions
|
||||
type ActionsService interface {
|
||||
List(context.Context, *ListOptions) ([]Action, *Response, error)
|
||||
Get(context.Context, int) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// ActionsServiceOp handles communition with the image action related methods of the
|
||||
// DigitalOcean API.
|
||||
type ActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ActionsService = &ActionsServiceOp{}
|
||||
|
||||
type actionsRoot struct {
|
||||
Actions []Action `json:"actions"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type actionRoot struct {
|
||||
Event *Action `json:"action"`
|
||||
}
|
||||
|
||||
// Action represents a DigitalOcean Action
|
||||
type Action struct {
|
||||
ID int `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
StartedAt *Timestamp `json:"started_at"`
|
||||
CompletedAt *Timestamp `json:"completed_at"`
|
||||
ResourceID int `json:"resource_id"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
RegionSlug string `json:"region_slug,omitempty"`
|
||||
}
|
||||
|
||||
// List all actions
|
||||
func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := actionsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Get an action by ID.
|
||||
func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", actionsBasePath, id)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (a Action) String() string {
|
||||
return Stringify(a)
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package 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}]}`)
|
||||
testMethod(t, r, http.MethodGet)
|
||||
})
|
||||
|
||||
actions, _, err := client.Actions.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
expected := []Action{{ID: 1}, {ID: 2}}
|
||||
if len(actions) != len(expected) || actions[0].ID != expected[0].ID || actions[1].ID != expected[1].ID {
|
||||
t.Fatalf("unexpected response")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const certificatesBasePath = "/v2/certificates"
|
||||
|
||||
// CertificatesService is an interface for managing certificates with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2/#certificates
|
||||
type CertificatesService interface {
|
||||
Get(context.Context, string) (*Certificate, *Response, error)
|
||||
List(context.Context, *ListOptions) ([]Certificate, *Response, error)
|
||||
Create(context.Context, *CertificateRequest) (*Certificate, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// Certificate represents a DigitalOcean certificate configuration.
|
||||
type Certificate struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
NotAfter string `json:"not_after,omitempty"`
|
||||
SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// CertificateRequest represents configuration for a new certificate.
|
||||
type CertificateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
PrivateKey string `json:"private_key,omitempty"`
|
||||
LeafCertificate string `json:"leaf_certificate,omitempty"`
|
||||
CertificateChain string `json:"certificate_chain,omitempty"`
|
||||
}
|
||||
|
||||
type certificateRoot struct {
|
||||
Certificate *Certificate `json:"certificate"`
|
||||
}
|
||||
|
||||
type certificatesRoot struct {
|
||||
Certificates []Certificate `json:"certificates"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// CertificatesServiceOp handles communication with certificates methods of the DigitalOcean API.
|
||||
type CertificatesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ CertificatesService = &CertificatesServiceOp{}
|
||||
|
||||
// Get an existing certificate by its identifier.
|
||||
func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) {
|
||||
urlStr := path.Join(certificatesBasePath, cID)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificateRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Certificate, resp, nil
|
||||
}
|
||||
|
||||
// List all certificates.
|
||||
func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Certificate, *Response, error) {
|
||||
urlStr, err := addOptions(certificatesBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificatesRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Certificates, resp, nil
|
||||
}
|
||||
|
||||
// Create a new certificate with provided configuration.
|
||||
func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) {
|
||||
req, err := c.client.NewRequest(ctx, http.MethodPost, certificatesBasePath, cr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(certificateRoot)
|
||||
resp, err := c.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Certificate, resp, nil
|
||||
}
|
||||
|
||||
// Delete a certificate by its identifier.
|
||||
func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) {
|
||||
urlStr := path.Join(certificatesBasePath, cID)
|
||||
|
||||
req, err := c.client.NewRequest(ctx, http.MethodDelete, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.client.Do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var certJSONResponse = `
|
||||
{
|
||||
"certificate": {
|
||||
"id": "892071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
"name": "web-cert-01",
|
||||
"not_after": "2017-02-22T00:23:00Z",
|
||||
"sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
"created_at": "2017-02-08T16:02:37Z"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var certsJSONResponse = `
|
||||
{
|
||||
"certificates": [
|
||||
{
|
||||
"id": "892071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
"name": "web-cert-01",
|
||||
"not_after": "2017-02-22T00:23:00Z",
|
||||
"sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
"created_at": "2017-02-08T16:02:37Z"
|
||||
},
|
||||
{
|
||||
"id": "992071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
"name": "web-cert-02",
|
||||
"not_after": "2017-02-22T00:23:00Z",
|
||||
"sha1_fingerprint": "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
"created_at": "2017-02-08T16:02:37Z"
|
||||
}
|
||||
],
|
||||
"links": {},
|
||||
"meta": {
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestCertificates_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
urlStr := "/v2/certificates"
|
||||
cID := "892071a0-bb95-49bc-8021-3afd67a210bf"
|
||||
urlStr = path.Join(urlStr, cID)
|
||||
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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",
|
||||
NotAfter: "2017-02-22T00:23:00Z",
|
||||
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
Created: "2017-02-08T16:02:37Z",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, certificate)
|
||||
}
|
||||
|
||||
func TestCertificates_List(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
urlStr := "/v2/certificates"
|
||||
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, certsJSONResponse)
|
||||
})
|
||||
|
||||
certificates, _, err := client.Certificates.List(ctx, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Certificates.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Certificate{
|
||||
{
|
||||
ID: "892071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
Name: "web-cert-01",
|
||||
NotAfter: "2017-02-22T00:23:00Z",
|
||||
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
Created: "2017-02-08T16:02:37Z",
|
||||
},
|
||||
{
|
||||
ID: "992071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
Name: "web-cert-02",
|
||||
NotAfter: "2017-02-22T00:23:00Z",
|
||||
SHA1Fingerprint: "cfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
Created: "2017-02-08T16:02:37Z",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, certificates)
|
||||
}
|
||||
|
||||
func TestCertificates_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &CertificateRequest{
|
||||
Name: "web-cert-01",
|
||||
PrivateKey: "-----BEGIN PRIVATE KEY-----",
|
||||
LeafCertificate: "-----BEGIN CERTIFICATE-----",
|
||||
CertificateChain: "-----BEGIN CERTIFICATE-----",
|
||||
}
|
||||
|
||||
urlStr := "/v2/certificates"
|
||||
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(CertificateRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, http.MethodPost)
|
||||
assert.Equal(t, createRequest, v)
|
||||
|
||||
fmt.Fprint(w, certJSONResponse)
|
||||
})
|
||||
|
||||
certificate, _, err := client.Certificates.Create(ctx, createRequest)
|
||||
if err != nil {
|
||||
t.Errorf("Certificates.Create returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := &Certificate{
|
||||
ID: "892071a0-bb95-49bc-8021-3afd67a210bf",
|
||||
Name: "web-cert-01",
|
||||
NotAfter: "2017-02-22T00:23:00Z",
|
||||
SHA1Fingerprint: "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
|
||||
Created: "2017-02-08T16:02:37Z",
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, certificate)
|
||||
}
|
||||
|
||||
func TestCertificates_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
cID := "892071a0-bb95-49bc-8021-3afd67a210bf"
|
||||
urlStr := "/v2/certificates"
|
||||
urlStr = path.Join(urlStr, cID)
|
||||
mux.HandleFunc(urlStr, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodDelete)
|
||||
})
|
||||
|
||||
_, err := client.Certificates.Delete(ctx, cID)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Certificates.Delete returned error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package context
|
||||
|
||||
import "time"
|
||||
|
||||
// A Context carries a deadline, a cancelation signal, and other values across
|
||||
// API boundaries.
|
||||
//
|
||||
// Context's methods may be called by multiple goroutines simultaneously.
|
||||
type Context interface {
|
||||
// Deadline returns the time when work done on behalf of this context
|
||||
// should be canceled. Deadline returns ok==false when no deadline is
|
||||
// set. Successive calls to Deadline return the same results.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that's closed when work done on behalf of this
|
||||
// context should be canceled. Done may return nil if this context can
|
||||
// never be canceled. Successive calls to Done return the same value.
|
||||
//
|
||||
// WithCancel arranges for Done to be closed when cancel is called;
|
||||
// WithDeadline arranges for Done to be closed when the deadline
|
||||
// expires; WithTimeout arranges for Done to be closed when the timeout
|
||||
// elapses.
|
||||
//
|
||||
// Done is provided for use in select statements:s
|
||||
//
|
||||
// // Stream generates values with DoSomething and sends them to out
|
||||
// // until DoSomething returns an error or ctx.Done is closed.
|
||||
// func Stream(ctx context.Context, out chan<- Value) error {
|
||||
// for {
|
||||
// v, err := DoSomething(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return ctx.Err()
|
||||
// case out <- v:
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// See http://blog.golang.org/pipelines for more examples of how to use
|
||||
// a Done channel for cancelation.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err returns a non-nil error value after Done is closed. Err returns
|
||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||
// context's deadline passed. No other values for Err are defined.
|
||||
// After Done is closed, successive calls to Err return the same value.
|
||||
Err() error
|
||||
|
||||
// Value returns the value associated with this context for key, or nil
|
||||
// if no value is associated with key. Successive calls to Value with
|
||||
// the same key returns the same result.
|
||||
//
|
||||
// Use context values only for request-scoped data that transits
|
||||
// processes and API boundaries, not for passing optional parameters to
|
||||
// functions.
|
||||
//
|
||||
// A key identifies a specific value in a Context. Functions that wish
|
||||
// to store values in Context typically allocate a key in a global
|
||||
// variable then use that key as the argument to context.WithValue and
|
||||
// Context.Value. A key can be any type that supports equality;
|
||||
// packages should define keys as an unexported type to avoid
|
||||
// collisions.
|
||||
//
|
||||
// Packages that define a Context key should provide type-safe accessors
|
||||
// for the values stores using that key:
|
||||
//
|
||||
// // Package user defines a User type that's stored in Contexts.
|
||||
// package user
|
||||
//
|
||||
// import "golang.org/x/net/context"
|
||||
//
|
||||
// // User is the type of value stored in the Contexts.
|
||||
// type User struct {...}
|
||||
//
|
||||
// // key is an unexported type for keys defined in this package.
|
||||
// // This prevents collisions with keys defined in other packages.
|
||||
// type key int
|
||||
//
|
||||
// // userKey is the key for user.User values in Contexts. It is
|
||||
// // unexported; clients use user.NewContext and user.FromContext
|
||||
// // instead of using this key directly.
|
||||
// var userKey key = 0
|
||||
//
|
||||
// // NewContext returns a new Context that carries value u.
|
||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||
// return context.WithValue(ctx, userKey, u)
|
||||
// }
|
||||
//
|
||||
// // FromContext returns the User value stored in ctx, if any.
|
||||
// func FromContext(ctx context.Context) (*User, bool) {
|
||||
// u, ok := ctx.Value(userKey).(*User)
|
||||
// return u, ok
|
||||
// }
|
||||
Value(key interface{}) interface{}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// +build go1.7
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DoRequest submits an HTTP request.
|
||||
func DoRequest(ctx Context, req *http.Request) (*http.Response, error) {
|
||||
return DoRequestWithClient(ctx, http.DefaultClient, req)
|
||||
}
|
||||
|
||||
// DoRequestWithClient submits an HTTP request using the specified client.
|
||||
func DoRequestWithClient(
|
||||
ctx Context,
|
||||
client *http.Client,
|
||||
req *http.Request) (*http.Response, error) {
|
||||
req = req.WithContext(ctx)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||
// it's unclear which Context to use or it is not yet available (because the
|
||||
// surrounding function has not yet been extended to accept a Context
|
||||
// parameter). TODO is recognized by static analysis tools that determine
|
||||
// whether Contexts are propagated correctly in a program.
|
||||
func TODO() Context {
|
||||
return context.TODO()
|
||||
}
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||
// values, and has no deadline. It is typically used by the main function,
|
||||
// initialization, and tests, and as the top-level Context for incoming
|
||||
// requests.
|
||||
func Background() Context {
|
||||
return context.Background()
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// +build !go1.7
|
||||
|
||||
package context
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
)
|
||||
|
||||
// DoRequest submits an HTTP request.
|
||||
func DoRequest(ctx Context, req *http.Request) (*http.Response, error) {
|
||||
return DoRequestWithClient(ctx, http.DefaultClient, req)
|
||||
}
|
||||
|
||||
// DoRequestWithClient submits an HTTP request using the specified client.
|
||||
func DoRequestWithClient(
|
||||
ctx Context,
|
||||
client *http.Client,
|
||||
req *http.Request) (*http.Response, error) {
|
||||
|
||||
return ctxhttp.Do(ctx, client, req)
|
||||
}
|
||||
|
||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||
// it's unclear which Context to use or it is not yet available (because the
|
||||
// surrounding function has not yet been extended to accept a Context
|
||||
// parameter). TODO is recognized by static analysis tools that determine
|
||||
// whether Contexts are propagated correctly in a program.
|
||||
func TODO() Context {
|
||||
return context.TODO()
|
||||
}
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||
// values, and has no deadline. It is typically used by the main function,
|
||||
// initialization, and tests, and as the top-level Context for incoming
|
||||
// requests.
|
||||
func Background() Context {
|
||||
return context.Background()
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// Package godo is the DigtalOcean API v2 client for Go
|
||||
package godo
|
|
@ -0,0 +1,330 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const domainsBasePath = "v2/domains"
|
||||
|
||||
// DomainsService is an interface for managing DNS with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#domains and
|
||||
// https://developers.digitalocean.com/documentation/v2#domain-records
|
||||
type DomainsService interface {
|
||||
List(context.Context, *ListOptions) ([]Domain, *Response, error)
|
||||
Get(context.Context, string) (*Domain, *Response, error)
|
||||
Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
|
||||
Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error)
|
||||
Record(context.Context, string, int) (*DomainRecord, *Response, error)
|
||||
DeleteRecord(context.Context, string, int) (*Response, error)
|
||||
EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
|
||||
CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
|
||||
}
|
||||
|
||||
// DomainsServiceOp handles communication with the domain related methods of the
|
||||
// DigitalOcean API.
|
||||
type DomainsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DomainsService = &DomainsServiceOp{}
|
||||
|
||||
// Domain represents a DigitalOcean domain
|
||||
type Domain struct {
|
||||
Name string `json:"name"`
|
||||
TTL int `json:"ttl"`
|
||||
ZoneFile string `json:"zone_file"`
|
||||
}
|
||||
|
||||
// domainRoot represents a response from the DigitalOcean API
|
||||
type domainRoot struct {
|
||||
Domain *Domain `json:"domain"`
|
||||
}
|
||||
|
||||
type domainsRoot struct {
|
||||
Domains []Domain `json:"domains"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DomainCreateRequest respresents a request to create a domain.
|
||||
type DomainCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
}
|
||||
|
||||
// DomainRecordRoot is the root of an individual Domain Record response
|
||||
type domainRecordRoot struct {
|
||||
DomainRecord *DomainRecord `json:"domain_record"`
|
||||
}
|
||||
|
||||
// DomainRecordsRoot is the root of a group of Domain Record responses
|
||||
type domainRecordsRoot struct {
|
||||
DomainRecords []DomainRecord `json:"domain_records"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DomainRecord represents a DigitalOcean DomainRecord
|
||||
type DomainRecord struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
}
|
||||
|
||||
// DomainRecordEditRequest represents a request to update a domain record.
|
||||
type DomainRecordEditRequest struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
TTL int `json:"ttl,omitempty"`
|
||||
Weight int `json:"weight,omitempty"`
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// List all domains.
|
||||
func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
|
||||
path := domainsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Domains, resp, err
|
||||
}
|
||||
|
||||
// Get individual domain. It requires a non-empty domain name.
|
||||
func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Response, error) {
|
||||
if len(name) < 1 {
|
||||
return nil, nil, NewArgError("name", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Domain, resp, err
|
||||
}
|
||||
|
||||
// Create a new domain
|
||||
func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCreateRequest) (*Domain, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := domainsBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Domain, resp, err
|
||||
}
|
||||
|
||||
// Delete domain
|
||||
func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
|
||||
if len(name) < 1 {
|
||||
return nil, NewArgError("name", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Converts a DomainRecord to a string.
|
||||
func (d DomainRecord) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Converts a DomainRecordEditRequest to a string.
|
||||
func (d DomainRecordEditRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Records returns a slice of DomainRecords for a domain
|
||||
func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(domainRecordsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.DomainRecords, resp, err
|
||||
}
|
||||
|
||||
// Record returns the record id from a domain
|
||||
func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
record := new(domainRecordRoot)
|
||||
resp, err := s.client.Do(ctx, req, record)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return record.DomainRecord, resp, err
|
||||
}
|
||||
|
||||
// DeleteRecord deletes a record from a domain identified by id
|
||||
func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// EditRecord edits a record using a DomainRecordEditRequest
|
||||
func (s *DomainsServiceOp) EditRecord(ctx context.Context,
|
||||
domain string,
|
||||
id int,
|
||||
editRequest *DomainRecordEditRequest,
|
||||
) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be an empty string")
|
||||
}
|
||||
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if editRequest == nil {
|
||||
return nil, nil, NewArgError("editRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, editRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
d := new(DomainRecord)
|
||||
resp, err := s.client.Do(ctx, req, d)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return d, resp, err
|
||||
}
|
||||
|
||||
// CreateRecord creates a record using a DomainRecordEditRequest
|
||||
func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
|
||||
domain string,
|
||||
createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
|
||||
if len(domain) < 1 {
|
||||
return nil, nil, NewArgError("domain", "cannot be empty string")
|
||||
}
|
||||
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
d := new(domainRecordRoot)
|
||||
resp, err := s.client.Do(ctx, req, d)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return d.DomainRecord, resp, err
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
package 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"}]}`)
|
||||
})
|
||||
|
||||
domains, _, err := client.Domains.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Domains.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Domain{{Name: "foo.com"}, {Name: "bar.com"}}
|
||||
if !reflect.DeepEqual(domains, expected) {
|
||||
t.Errorf("Domains.List returned %+v, expected %+v", domains, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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, "PUT")
|
||||
if !reflect.DeepEqual(v, editRequest) {
|
||||
t.Errorf("Request body = %+v, expected %+v", v, editRequest)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `{"id":1}`)
|
||||
})
|
||||
|
||||
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}
|
||||
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,
|
||||
}
|
||||
|
||||
stringified := record.String()
|
||||
expected := `godo.DomainRecord{ID:1, Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10}`
|
||||
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,
|
||||
}
|
||||
|
||||
stringified := record.String()
|
||||
expected := `godo.DomainRecordEditRequest{Type:"CNAME", Name:"example", Data:"@", Priority:10, Port:10, TTL:1800, Weight:10}`
|
||||
if expected != stringified {
|
||||
t.Errorf("DomainRecordEditRequest.String returned %+v, expected %+v", stringified, expected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// ActionRequest reprents DigitalOcean Action Request
|
||||
type ActionRequest map[string]interface{}
|
||||
|
||||
// DropletActionsService is an interface for interfacing with the Droplet actions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#droplet-actions
|
||||
type DropletActionsService interface {
|
||||
Shutdown(context.Context, int) (*Action, *Response, error)
|
||||
ShutdownByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerOff(context.Context, int) (*Action, *Response, error)
|
||||
PowerOffByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerOn(context.Context, int) (*Action, *Response, error)
|
||||
PowerOnByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PowerCycle(context.Context, int) (*Action, *Response, error)
|
||||
PowerCycleByTag(context.Context, string) ([]Action, *Response, error)
|
||||
Reboot(context.Context, int) (*Action, *Response, error)
|
||||
Restore(context.Context, int, int) (*Action, *Response, error)
|
||||
Resize(context.Context, int, string, bool) (*Action, *Response, error)
|
||||
Rename(context.Context, int, string) (*Action, *Response, error)
|
||||
Snapshot(context.Context, int, string) (*Action, *Response, error)
|
||||
SnapshotByTag(context.Context, string, string) ([]Action, *Response, error)
|
||||
EnableBackups(context.Context, int) (*Action, *Response, error)
|
||||
EnableBackupsByTag(context.Context, string) ([]Action, *Response, error)
|
||||
DisableBackups(context.Context, int) (*Action, *Response, error)
|
||||
DisableBackupsByTag(context.Context, string) ([]Action, *Response, error)
|
||||
PasswordReset(context.Context, int) (*Action, *Response, error)
|
||||
RebuildByImageID(context.Context, int, int) (*Action, *Response, error)
|
||||
RebuildByImageSlug(context.Context, int, string) (*Action, *Response, error)
|
||||
ChangeKernel(context.Context, int, int) (*Action, *Response, error)
|
||||
EnableIPv6(context.Context, int) (*Action, *Response, error)
|
||||
EnableIPv6ByTag(context.Context, string) ([]Action, *Response, error)
|
||||
EnablePrivateNetworking(context.Context, int) (*Action, *Response, error)
|
||||
EnablePrivateNetworkingByTag(context.Context, string) ([]Action, *Response, error)
|
||||
Upgrade(context.Context, int) (*Action, *Response, error)
|
||||
Get(context.Context, int, int) (*Action, *Response, error)
|
||||
GetByURI(context.Context, string) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// DropletActionsServiceOp handles communication with the Droplet action related
|
||||
// methods of the DigitalOcean API.
|
||||
type DropletActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DropletActionsService = &DropletActionsServiceOp{}
|
||||
|
||||
// Shutdown a Droplet
|
||||
func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "shutdown"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// ShutdownByTag shuts down Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "shutdown"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerOff a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerOff(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_off"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerOffByTag powers off Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_off"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerOn a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerOn(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_on"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerOnByTag powers on Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_on"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PowerCycle a Droplet
|
||||
func (s *DropletActionsServiceOp) PowerCycle(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_cycle"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// PowerCycleByTag power cycles Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "power_cycle"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// Reboot a Droplet
|
||||
func (s *DropletActionsServiceOp) Reboot(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "reboot"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Restore an image to a Droplet
|
||||
func (s *DropletActionsServiceOp) Restore(ctx context.Context, id, imageID int) (*Action, *Response, error) {
|
||||
requestType := "restore"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"image": float64(imageID),
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Resize a Droplet
|
||||
func (s *DropletActionsServiceOp) Resize(ctx context.Context, id int, sizeSlug string, resizeDisk bool) (*Action, *Response, error) {
|
||||
requestType := "resize"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"size": sizeSlug,
|
||||
"disk": resizeDisk,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Rename a Droplet
|
||||
func (s *DropletActionsServiceOp) Rename(ctx context.Context, id int, name string) (*Action, *Response, error) {
|
||||
requestType := "rename"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// Snapshot a Droplet.
|
||||
func (s *DropletActionsServiceOp) Snapshot(ctx context.Context, id int, name string) (*Action, *Response, error) {
|
||||
requestType := "snapshot"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// SnapshotByTag snapshots Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) ([]Action, *Response, error) {
|
||||
requestType := "snapshot"
|
||||
request := &ActionRequest{
|
||||
"type": requestType,
|
||||
"name": name,
|
||||
}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// EnableBackups enables backups for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnableBackups(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_backups"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableBackupsByTag enables backups for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_backups"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// DisableBackups disables backups for a Droplet.
|
||||
func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "disable_backups"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// DisableBackupsByTag disables backups for Droplet matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "disable_backups"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// PasswordReset resets the password for a Droplet.
|
||||
func (s *DropletActionsServiceOp) PasswordReset(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "password_reset"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// RebuildByImageID rebuilds a Droplet from an image with a given id.
|
||||
func (s *DropletActionsServiceOp) RebuildByImageID(ctx context.Context, id, imageID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "rebuild", "image": imageID}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// RebuildByImageSlug rebuilds a Droplet from an Image matched by a given Slug.
|
||||
func (s *DropletActionsServiceOp) RebuildByImageSlug(ctx context.Context, id int, slug string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "rebuild", "image": slug}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// ChangeKernel changes the kernel for a Droplet.
|
||||
func (s *DropletActionsServiceOp) ChangeKernel(ctx context.Context, id, kernelID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "change_kernel", "kernel": kernelID}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableIPv6 enables IPv6 for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_ipv6"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_ipv6"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// EnablePrivateNetworking enables private networking for a Droplet.
|
||||
func (s *DropletActionsServiceOp) EnablePrivateNetworking(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_private_networking"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
// EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag.
|
||||
func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "enable_private_networking"}
|
||||
return s.doActionByTag(ctx, tag, request)
|
||||
}
|
||||
|
||||
// Upgrade a Droplet.
|
||||
func (s *DropletActionsServiceOp) Upgrade(ctx context.Context, id int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "upgrade"}
|
||||
return s.doAction(ctx, id, request)
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request *ActionRequest) (*Action, *Response, error) {
|
||||
if id < 1 {
|
||||
return nil, nil, NewArgError("id", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if request == nil {
|
||||
return nil, nil, NewArgError("request", "request can't be nil")
|
||||
}
|
||||
|
||||
path := dropletActionPath(id)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) ([]Action, *Response, error) {
|
||||
if tag == "" {
|
||||
return nil, nil, NewArgError("tag", "cannot be empty")
|
||||
}
|
||||
|
||||
if request == nil {
|
||||
return nil, nil, NewArgError("request", "request can't be nil")
|
||||
}
|
||||
|
||||
path := dropletActionPathByTag(tag)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Get an action for a particular Droplet by id.
|
||||
func (s *DropletActionsServiceOp) Get(ctx context.Context, dropletID, actionID int) (*Action, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if actionID < 1 {
|
||||
return nil, nil, NewArgError("actionID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletActionPath(dropletID), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// GetByURI gets an action for a particular Droplet by id.
|
||||
func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) (*Action, *Response, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.get(ctx, u.Path)
|
||||
|
||||
}
|
||||
|
||||
func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
|
||||
}
|
||||
|
||||
func dropletActionPath(dropletID int) string {
|
||||
return fmt.Sprintf("v2/droplets/%d/actions", dropletID)
|
||||
}
|
||||
|
||||
func dropletActionPathByTag(tag string) string {
|
||||
return fmt.Sprintf("v2/droplets/actions?tag_name=%s", tag)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,567 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const dropletBasePath = "v2/droplets"
|
||||
|
||||
var errNoNetworks = errors.New("no networks have been defined")
|
||||
|
||||
// DropletsService is an interface for interfacing with the Droplet
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#droplets
|
||||
type DropletsService interface {
|
||||
List(context.Context, *ListOptions) ([]Droplet, *Response, error)
|
||||
ListByTag(context.Context, string, *ListOptions) ([]Droplet, *Response, error)
|
||||
Get(context.Context, int) (*Droplet, *Response, error)
|
||||
Create(context.Context, *DropletCreateRequest) (*Droplet, *Response, error)
|
||||
CreateMultiple(context.Context, *DropletMultiCreateRequest) ([]Droplet, *Response, error)
|
||||
Delete(context.Context, int) (*Response, error)
|
||||
DeleteByTag(context.Context, string) (*Response, error)
|
||||
Kernels(context.Context, int, *ListOptions) ([]Kernel, *Response, error)
|
||||
Snapshots(context.Context, int, *ListOptions) ([]Image, *Response, error)
|
||||
Backups(context.Context, int, *ListOptions) ([]Image, *Response, error)
|
||||
Actions(context.Context, int, *ListOptions) ([]Action, *Response, error)
|
||||
Neighbors(context.Context, int) ([]Droplet, *Response, error)
|
||||
}
|
||||
|
||||
// DropletsServiceOp handles communication with the Droplet related methods of the
|
||||
// DigitalOcean API.
|
||||
type DropletsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ DropletsService = &DropletsServiceOp{}
|
||||
|
||||
// Droplet represents a DigitalOcean Droplet
|
||||
type Droplet struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Memory int `json:"memory,omitempty"`
|
||||
Vcpus int `json:"vcpus,omitempty"`
|
||||
Disk int `json:"disk,omitempty"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
Image *Image `json:"image,omitempty"`
|
||||
Size *Size `json:"size,omitempty"`
|
||||
SizeSlug string `json:"size_slug,omitempty"`
|
||||
BackupIDs []int `json:"backup_ids,omitempty"`
|
||||
NextBackupWindow *BackupWindow `json:"next_backup_window,omitempty"`
|
||||
SnapshotIDs []int `json:"snapshot_ids,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
Locked bool `json:"locked,bool,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Networks *Networks `json:"networks,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
Kernel *Kernel `json:"kernel,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
VolumeIDs []string `json:"volume_ids"`
|
||||
}
|
||||
|
||||
// PublicIPv4 returns the public IPv4 address for the Droplet.
|
||||
func (d *Droplet) PublicIPv4() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v4 := range d.Networks.V4 {
|
||||
if v4.Type == "public" {
|
||||
return v4.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PrivateIPv4 returns the private IPv4 address for the Droplet.
|
||||
func (d *Droplet) PrivateIPv4() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v4 := range d.Networks.V4 {
|
||||
if v4.Type == "private" {
|
||||
return v4.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PublicIPv6 returns the public IPv6 address for the Droplet.
|
||||
func (d *Droplet) PublicIPv6() (string, error) {
|
||||
if d.Networks == nil {
|
||||
return "", errNoNetworks
|
||||
}
|
||||
|
||||
for _, v6 := range d.Networks.V6 {
|
||||
if v6.Type == "public" {
|
||||
return v6.IPAddress, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Kernel object
|
||||
type Kernel struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// BackupWindow object
|
||||
type BackupWindow struct {
|
||||
Start *Timestamp `json:"start,omitempty"`
|
||||
End *Timestamp `json:"end,omitempty"`
|
||||
}
|
||||
|
||||
// Convert Droplet to a string
|
||||
func (d Droplet) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// DropletRoot represents a Droplet root
|
||||
type dropletRoot struct {
|
||||
Droplet *Droplet `json:"droplet"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
type dropletsRoot struct {
|
||||
Droplets []Droplet `json:"droplets"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type kernelsRoot struct {
|
||||
Kernels []Kernel `json:"kernels,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type dropletSnapshotsRoot struct {
|
||||
Snapshots []Image `json:"snapshots,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type backupsRoot struct {
|
||||
Backups []Image `json:"backups,omitempty"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// DropletCreateImage identifies an image for the create request. It prefers slug over ID.
|
||||
type DropletCreateImage struct {
|
||||
ID int
|
||||
Slug string
|
||||
}
|
||||
|
||||
// MarshalJSON returns either the slug or id of the image. It returns the id
|
||||
// if the slug is empty.
|
||||
func (d DropletCreateImage) MarshalJSON() ([]byte, error) {
|
||||
if d.Slug != "" {
|
||||
return json.Marshal(d.Slug)
|
||||
}
|
||||
|
||||
return json.Marshal(d.ID)
|
||||
}
|
||||
|
||||
// DropletCreateVolume identifies a volume to attach for the create request. It
|
||||
// prefers Name over ID,
|
||||
type DropletCreateVolume struct {
|
||||
ID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// MarshalJSON returns an object with either the name or id of the volume. It
|
||||
// returns the id if the name is empty.
|
||||
func (d DropletCreateVolume) MarshalJSON() ([]byte, error) {
|
||||
if d.Name != "" {
|
||||
return json.Marshal(struct {
|
||||
Name string `json:"name"`
|
||||
}{Name: d.Name})
|
||||
}
|
||||
|
||||
return json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
}{ID: d.ID})
|
||||
}
|
||||
|
||||
// DropletCreateSSHKey identifies a SSH Key for the create request. It prefers fingerprint over ID.
|
||||
type DropletCreateSSHKey struct {
|
||||
ID int
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// MarshalJSON returns either the fingerprint or id of the ssh key. It returns
|
||||
// the id if the fingerprint is empty.
|
||||
func (d DropletCreateSSHKey) MarshalJSON() ([]byte, error) {
|
||||
if d.Fingerprint != "" {
|
||||
return json.Marshal(d.Fingerprint)
|
||||
}
|
||||
|
||||
return json.Marshal(d.ID)
|
||||
}
|
||||
|
||||
// DropletCreateRequest represents a request to create a Droplet.
|
||||
type DropletCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
Region string `json:"region"`
|
||||
Size string `json:"size"`
|
||||
Image DropletCreateImage `json:"image"`
|
||||
SSHKeys []DropletCreateSSHKey `json:"ssh_keys"`
|
||||
Backups bool `json:"backups"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
PrivateNetworking bool `json:"private_networking"`
|
||||
Monitoring bool `json:"monitoring"`
|
||||
UserData string `json:"user_data,omitempty"`
|
||||
Volumes []DropletCreateVolume `json:"volumes,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// DropletMultiCreateRequest is a request to create multiple Droplets.
|
||||
type DropletMultiCreateRequest struct {
|
||||
Names []string `json:"names"`
|
||||
Region string `json:"region"`
|
||||
Size string `json:"size"`
|
||||
Image DropletCreateImage `json:"image"`
|
||||
SSHKeys []DropletCreateSSHKey `json:"ssh_keys"`
|
||||
Backups bool `json:"backups"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
PrivateNetworking bool `json:"private_networking"`
|
||||
Monitoring bool `json:"monitoring"`
|
||||
UserData string `json:"user_data,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (d DropletCreateRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
func (d DropletMultiCreateRequest) String() string {
|
||||
return Stringify(d)
|
||||
}
|
||||
|
||||
// Networks represents the Droplet's Networks.
|
||||
type Networks struct {
|
||||
V4 []NetworkV4 `json:"v4,omitempty"`
|
||||
V6 []NetworkV6 `json:"v6,omitempty"`
|
||||
}
|
||||
|
||||
// NetworkV4 represents a DigitalOcean IPv4 Network.
|
||||
type NetworkV4 struct {
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Netmask string `json:"netmask,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (n NetworkV4) String() string {
|
||||
return Stringify(n)
|
||||
}
|
||||
|
||||
// NetworkV6 represents a DigitalOcean IPv6 network.
|
||||
type NetworkV6 struct {
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
Netmask int `json:"netmask,omitempty"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (n NetworkV6) String() string {
|
||||
return Stringify(n)
|
||||
}
|
||||
|
||||
// Performs a list request given a path.
|
||||
func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
// List all Droplets.
|
||||
func (s *DropletsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Droplet, *Response, error) {
|
||||
path := dropletBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// ListByTag lists all Droplets matched by a Tag.
|
||||
func (s *DropletsServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Droplet, *Response, error) {
|
||||
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// Get individual Droplet.
|
||||
func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Droplet, resp, err
|
||||
}
|
||||
|
||||
// Create Droplet
|
||||
func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCreateRequest) (*Droplet, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := dropletBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Droplet, resp, err
|
||||
}
|
||||
|
||||
// CreateMultiple creates multiple Droplets.
|
||||
func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := dropletBasePath
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
// Performs a delete request given a path
|
||||
func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Delete Droplet.
|
||||
func (s *DropletsServiceOp) Delete(ctx context.Context, dropletID int) (*Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
|
||||
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// DeleteByTag deletes Droplets matched by a Tag.
|
||||
func (s *DropletsServiceOp) DeleteByTag(ctx context.Context, tag string) (*Response, error) {
|
||||
if tag == "" {
|
||||
return nil, NewArgError("tag", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s?tag_name=%s", dropletBasePath, tag)
|
||||
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// Kernels lists kernels available for a Droplet.
|
||||
func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *ListOptions) ([]Kernel, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/kernels", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(kernelsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Kernels, resp, err
|
||||
}
|
||||
|
||||
// Actions lists the actions for a Droplet.
|
||||
func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *ListOptions) ([]Action, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/actions", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
// Backups lists the backups for a Droplet.
|
||||
func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/backups", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(backupsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Backups, resp, err
|
||||
}
|
||||
|
||||
// Snapshots lists the snapshots available for a Droplet.
|
||||
func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *ListOptions) ([]Image, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/snapshots", dropletBasePath, dropletID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletSnapshotsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, err
|
||||
}
|
||||
|
||||
// Neighbors lists the neighbors for a Droplet.
|
||||
func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Droplet, *Response, error) {
|
||||
if dropletID < 1 {
|
||||
return nil, nil, NewArgError("dropletID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(dropletsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Droplets, resp, err
|
||||
}
|
||||
|
||||
func (s *DropletsServiceOp) dropletActionStatus(ctx context.Context, uri string) (string, error) {
|
||||
action, _, err := s.client.DropletActions.GetByURI(ctx, uri)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return action.Status, nil
|
||||
}
|
|
@ -0,0 +1,468 @@
|
|||
package 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}]}`)
|
||||
})
|
||||
|
||||
droplets, _, err := client.Droplets.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Droplet{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(droplets, expected) {
|
||||
t.Errorf("Droplets.List\n got=%#v\nwant=%#v", droplets, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDroplets_ListDropletsByTag(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("tag_name") != "testing-1" {
|
||||
t.Errorf("Droplets.ListByTag did not request with a tag parameter")
|
||||
}
|
||||
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, `{"droplets": [{"id":1},{"id":2}]}`)
|
||||
})
|
||||
|
||||
droplets, _, err := client.Droplets.ListByTag(ctx, "testing-1", nil)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.ListByTag returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Droplet{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(droplets, expected) {
|
||||
t.Errorf("Droplets.ListByTag returned %+v, expected %+v", droplets, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
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"},
|
||||
}
|
||||
|
||||
var v map[string]interface{}
|
||||
err := json.NewDecoder(r.Body).Decode(&v)
|
||||
if err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(v, expected) {
|
||||
t.Errorf("Request body\n got=%#v\nwant=%#v", v, expected)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `{"droplet":{"id":1}, "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "create"}]}}`)
|
||||
})
|
||||
|
||||
droplet, resp, err := client.Droplets.Create(ctx, createRequest)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Create returned error: %v", err)
|
||||
}
|
||||
|
||||
if id := droplet.ID; id != 1 {
|
||||
t.Errorf("expected id '%d', received '%d'", 1, id)
|
||||
}
|
||||
|
||||
if a := resp.Links.Actions[0]; a.ID != 1 {
|
||||
t.Errorf("expected action id '%d', received '%d'", 1, a.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDroplets_CreateMultiple(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &DropletMultiCreateRequest{
|
||||
Names: []string{"name1", "name2"},
|
||||
Region: "region",
|
||||
Size: "size",
|
||||
Image: DropletCreateImage{
|
||||
ID: 1,
|
||||
},
|
||||
Tags: []string{"one", "two"},
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/droplets", func(w http.ResponseWriter, r *http.Request) {
|
||||
expected := map[string]interface{}{
|
||||
"names": []interface{}{"name1", "name2"},
|
||||
"region": "region",
|
||||
"size": "size",
|
||||
"image": float64(1),
|
||||
"ssh_keys": nil,
|
||||
"backups": false,
|
||||
"ipv6": false,
|
||||
"private_networking": false,
|
||||
"monitoring": false,
|
||||
"tags": []interface{}{"one", "two"},
|
||||
}
|
||||
|
||||
var v map[string]interface{}
|
||||
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, `{"droplets":[{"id":1},{"id":2}], "links":{"actions": [{"id": 1, "href": "http://example.com", "rel": "multiple_create"}]}}`)
|
||||
})
|
||||
|
||||
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'", 1, 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}]}`)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
kernels, _, err := client.Droplets.Kernels(ctx, 12345, opt)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Kernels returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Kernel{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(kernels, expected) {
|
||||
t.Errorf("Droplets.Kernels\n got=%#v\nwant=%#v", kernels, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
snapshots, _, err := client.Droplets.Snapshots(ctx, 12345, opt)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Snapshots returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(snapshots, expected) {
|
||||
t.Errorf("Droplets.Snapshots\n got=%#v\nwant=%#v", snapshots, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
backups, _, err := client.Droplets.Backups(ctx, 12345, opt)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Backups returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(backups, expected) {
|
||||
t.Errorf("Droplets.Backups\n got=%#v\nwant=%#v", backups, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
opt := &ListOptions{Page: 2}
|
||||
actions, _, err := client.Droplets.Actions(ctx, 12345, opt)
|
||||
if err != nil {
|
||||
t.Errorf("Droplets.Actions returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Action{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(actions, expected) {
|
||||
t.Errorf("Droplets.Actions\n got=%#v\nwant=%#v", actions, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package godo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ArgError is an error that represents an error with an input to godo. It
|
||||
// identifies the argument and the cause (if possible).
|
||||
type ArgError struct {
|
||||
arg string
|
||||
reason string
|
||||
}
|
||||
|
||||
var _ error = &ArgError{}
|
||||
|
||||
// NewArgError creates an InputError.
|
||||
func NewArgError(arg, reason string) *ArgError {
|
||||
return &ArgError{
|
||||
arg: arg,
|
||||
reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ArgError) Error() string {
|
||||
return fmt.Sprintf("%s is invalid because %s", e.arg, e.reason)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const firewallsBasePath = "/v2/firewalls"
|
||||
|
||||
// FirewallsService is an interface for managing Firewalls with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls
|
||||
type FirewallsService interface {
|
||||
Get(context.Context, string) (*Firewall, *Response, error)
|
||||
Create(context.Context, *FirewallRequest) (*Firewall, *Response, error)
|
||||
Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
List(context.Context, *ListOptions) ([]Firewall, *Response, error)
|
||||
ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error)
|
||||
AddDroplets(context.Context, string, ...int) (*Response, error)
|
||||
RemoveDroplets(context.Context, string, ...int) (*Response, error)
|
||||
AddTags(context.Context, string, ...string) (*Response, error)
|
||||
RemoveTags(context.Context, string, ...string) (*Response, error)
|
||||
AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
|
||||
RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
|
||||
}
|
||||
|
||||
// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API.
|
||||
type FirewallsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Firewall represents a DigitalOcean Firewall configuration.
|
||||
type Firewall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
Tags []string `json:"tags"`
|
||||
Created string `json:"created_at"`
|
||||
PendingChanges []PendingChange `json:"pending_changes"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a Firewall.
|
||||
func (fw Firewall) String() string {
|
||||
return Stringify(fw)
|
||||
}
|
||||
|
||||
// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
|
||||
type FirewallRequest struct {
|
||||
Name string `json:"name"`
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall.
|
||||
type FirewallRulesRequest struct {
|
||||
InboundRules []InboundRule `json:"inbound_rules"`
|
||||
OutboundRules []OutboundRule `json:"outbound_rules"`
|
||||
}
|
||||
|
||||
// InboundRule represents a DigitalOcean Firewall inbound rule.
|
||||
type InboundRule struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
PortRange string `json:"ports,omitempty"`
|
||||
Sources *Sources `json:"sources"`
|
||||
}
|
||||
|
||||
// OutboundRule represents a DigitalOcean Firewall outbound rule.
|
||||
type OutboundRule struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
PortRange string `json:"ports,omitempty"`
|
||||
Destinations *Destinations `json:"destinations"`
|
||||
}
|
||||
|
||||
// Sources represents a DigitalOcean Firewall InboundRule sources.
|
||||
type Sources struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
|
||||
}
|
||||
|
||||
// PendingChange represents a DigitalOcean Firewall status details.
|
||||
type PendingChange struct {
|
||||
DropletID int `json:"droplet_id,omitempty"`
|
||||
Removing bool `json:"removing,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// Destinations represents a DigitalOcean Firewall OutboundRule destinations.
|
||||
type Destinations struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
|
||||
}
|
||||
|
||||
var _ FirewallsService = &FirewallsServiceOp{}
|
||||
|
||||
// Get an existing Firewall by its identifier.
|
||||
func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Create a new Firewall with a given configuration.
|
||||
func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Update an existing Firewall with new configuration.
|
||||
func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
|
||||
req, err := fw.client.NewRequest(ctx, "PUT", path, fr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Firewall, resp, err
|
||||
}
|
||||
|
||||
// Delete a Firewall by its identifier.
|
||||
func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID)
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, nil)
|
||||
}
|
||||
|
||||
// List Firewalls.
|
||||
func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) {
|
||||
path, err := addOptions(firewallsBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return fw.listHelper(ctx, path)
|
||||
}
|
||||
|
||||
// ListByDroplet Firewalls.
|
||||
func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) {
|
||||
basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls")
|
||||
path, err := addOptions(basePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return fw.listHelper(ctx, path)
|
||||
}
|
||||
|
||||
// AddDroplets to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "droplets")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs})
|
||||
}
|
||||
|
||||
// RemoveDroplets from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "droplets")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs})
|
||||
}
|
||||
|
||||
// AddTags to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "tags")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags})
|
||||
}
|
||||
|
||||
// RemoveTags from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "tags")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags})
|
||||
}
|
||||
|
||||
// AddRules to a Firewall.
|
||||
func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "rules")
|
||||
return fw.createAndDoReq(ctx, http.MethodPost, path, rr)
|
||||
}
|
||||
|
||||
// RemoveRules from a Firewall.
|
||||
func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
|
||||
path := path.Join(firewallsBasePath, fID, "rules")
|
||||
return fw.createAndDoReq(ctx, http.MethodDelete, path, rr)
|
||||
}
|
||||
|
||||
type dropletsRequest struct {
|
||||
IDs []int `json:"droplet_ids"`
|
||||
}
|
||||
|
||||
type tagsRequest struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type firewallRoot struct {
|
||||
Firewall *Firewall `json:"firewall"`
|
||||
}
|
||||
|
||||
type firewallsRoot struct {
|
||||
Firewalls []Firewall `json:"firewalls"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, method, path, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fw.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) {
|
||||
req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(firewallsRoot)
|
||||
resp, err := fw.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Firewalls, resp, err
|
||||
}
|
|
@ -0,0 +1,839 @@
|
|||
package 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, _, 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 %+v, expected %+v", actualFirewalls, expectedFirewalls)
|
||||
}
|
||||
}
|
||||
|
||||
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, _, 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 %+v, expected %+v", actualFirewalls, expectedFirewalls)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const floatingBasePath = "v2/floating_ips"
|
||||
|
||||
// FloatingIPsService is an interface for interfacing with the floating IPs
|
||||
// endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#floating-ips
|
||||
type FloatingIPsService interface {
|
||||
List(context.Context, *ListOptions) ([]FloatingIP, *Response, error)
|
||||
Get(context.Context, string) (*FloatingIP, *Response, error)
|
||||
Create(context.Context, *FloatingIPCreateRequest) (*FloatingIP, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// FloatingIPsServiceOp handles communication with the floating IPs related methods of the
|
||||
// DigitalOcean API.
|
||||
type FloatingIPsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ FloatingIPsService = &FloatingIPsServiceOp{}
|
||||
|
||||
// FloatingIP represents a Digital Ocean floating IP.
|
||||
type FloatingIP struct {
|
||||
Region *Region `json:"region"`
|
||||
Droplet *Droplet `json:"droplet"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
func (f FloatingIP) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
type floatingIPsRoot struct {
|
||||
FloatingIPs []FloatingIP `json:"floating_ips"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type floatingIPRoot struct {
|
||||
FloatingIP *FloatingIP `json:"floating_ip"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// FloatingIPCreateRequest represents a request to create a floating IP.
|
||||
// If DropletID is not empty, the floating IP will be assigned to the
|
||||
// droplet.
|
||||
type FloatingIPCreateRequest struct {
|
||||
Region string `json:"region"`
|
||||
DropletID int `json:"droplet_id,omitempty"`
|
||||
}
|
||||
|
||||
// List all floating IPs.
|
||||
func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]FloatingIP, *Response, error) {
|
||||
path := floatingBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPsRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.FloatingIPs, resp, err
|
||||
}
|
||||
|
||||
// Get an individual floating IP.
|
||||
func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.FloatingIP, resp, err
|
||||
}
|
||||
|
||||
// Create a floating IP. If the DropletID field of the request is not empty,
|
||||
// the floating IP will also be assigned to the droplet.
|
||||
func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) {
|
||||
path := floatingBasePath
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(floatingIPRoot)
|
||||
resp, err := f.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.FloatingIP, resp, err
|
||||
}
|
||||
|
||||
// Delete a floating IP.
|
||||
func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
|
||||
|
||||
req, err := f.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := f.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// FloatingIPActionsService is an interface for interfacing with the
|
||||
// floating IPs actions endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#floating-ips-action
|
||||
type FloatingIPActionsService interface {
|
||||
Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error)
|
||||
Unassign(ctx context.Context, ip string) (*Action, *Response, error)
|
||||
Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error)
|
||||
List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error)
|
||||
}
|
||||
|
||||
// FloatingIPActionsServiceOp handles communication with the floating IPs
|
||||
// action related methods of the DigitalOcean API.
|
||||
type FloatingIPActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Assign a floating IP to a droplet.
|
||||
func (s *FloatingIPActionsServiceOp) Assign(ctx context.Context, ip string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "assign",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, ip, request)
|
||||
}
|
||||
|
||||
// Unassign a floating IP from the droplet it is currently assigned to.
|
||||
func (s *FloatingIPActionsServiceOp) Unassign(ctx context.Context, ip string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{"type": "unassign"}
|
||||
return s.doAction(ctx, ip, request)
|
||||
}
|
||||
|
||||
// Get an action for a particular floating IP by id.
|
||||
func (s *FloatingIPActionsServiceOp) Get(ctx context.Context, ip string, actionID int) (*Action, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%d", floatingIPActionPath(ip), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// List the actions for a particular floating IP.
|
||||
func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := floatingIPActionPath(ip)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
|
||||
path := floatingIPActionPath(ip)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
func floatingIPActionPath(ip string) string {
|
||||
return fmt.Sprintf("%s/%s/actions", floatingBasePath, ip)
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloatingIPsActions_Assign(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
dropletID := 12345
|
||||
assignRequest := &ActionRequest{
|
||||
"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
|
||||
"type": "assign",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/floating_ips/192.168.0.1/actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(ActionRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloatingIPs_ListFloatingIPs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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"}]}`)
|
||||
})
|
||||
|
||||
floatingIPs, _, err := client.FloatingIPs.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("FloatingIPs.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []FloatingIP{
|
||||
{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 1}, IP: "192.168.0.1"},
|
||||
{Region: &Region{Slug: "nyc3"}, Droplet: &Droplet{ID: 2}, IP: "192.168.0.2"},
|
||||
}
|
||||
if !reflect.DeepEqual(floatingIPs, expected) {
|
||||
t.Errorf("FloatingIPs.List returned %+v, expected %+v", floatingIPs, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloatingIPs_ListFloatingIPsMultiplePages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/floating_ips", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,400 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
headerLink "github.com/tent/http-link-go"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const (
|
||||
libraryVersion = "1.1.0"
|
||||
defaultBaseURL = "https://api.digitalocean.com/"
|
||||
userAgent = "godo/" + libraryVersion
|
||||
mediaType = "application/json"
|
||||
|
||||
headerRateLimit = "RateLimit-Limit"
|
||||
headerRateRemaining = "RateLimit-Remaining"
|
||||
headerRateReset = "RateLimit-Reset"
|
||||
)
|
||||
|
||||
// Client manages communication with DigitalOcean V2 API.
|
||||
type Client struct {
|
||||
// HTTP client used to communicate with the DO API.
|
||||
client *http.Client
|
||||
|
||||
// Base URL for API requests.
|
||||
BaseURL *url.URL
|
||||
|
||||
// User agent for client
|
||||
UserAgent string
|
||||
|
||||
// Rate contains the current rate limit for the client as determined by the most recent
|
||||
// API call.
|
||||
Rate Rate
|
||||
|
||||
// Services used for communicating with the API
|
||||
Account AccountService
|
||||
Actions ActionsService
|
||||
Domains DomainsService
|
||||
Droplets DropletsService
|
||||
DropletActions DropletActionsService
|
||||
Images ImagesService
|
||||
ImageActions ImageActionsService
|
||||
Keys KeysService
|
||||
Regions RegionsService
|
||||
Sizes SizesService
|
||||
FloatingIPs FloatingIPsService
|
||||
FloatingIPActions FloatingIPActionsService
|
||||
Snapshots SnapshotsService
|
||||
Storage StorageService
|
||||
StorageActions StorageActionsService
|
||||
Tags TagsService
|
||||
LoadBalancers LoadBalancersService
|
||||
Certificates CertificatesService
|
||||
Firewalls FirewallsService
|
||||
|
||||
// Optional function called after every successful request made to the DO APIs
|
||||
onRequestCompleted RequestCompletionCallback
|
||||
}
|
||||
|
||||
// RequestCompletionCallback defines the type of the request callback function
|
||||
type RequestCompletionCallback func(*http.Request, *http.Response)
|
||||
|
||||
// ListOptions specifies the optional parameters to various List methods that
|
||||
// support pagination.
|
||||
type ListOptions struct {
|
||||
// For paginated result sets, page of results to retrieve.
|
||||
Page int `url:"page,omitempty"`
|
||||
|
||||
// For paginated result sets, the number of results to include per page.
|
||||
PerPage int `url:"per_page,omitempty"`
|
||||
}
|
||||
|
||||
// Response is a DigitalOcean response. This wraps the standard http.Response returned from DigitalOcean.
|
||||
type Response struct {
|
||||
*http.Response
|
||||
|
||||
// Links that were returned with the response. These are parsed from
|
||||
// request body and not the header.
|
||||
Links *Links
|
||||
|
||||
// Monitoring URI
|
||||
Monitor string
|
||||
|
||||
Rate
|
||||
}
|
||||
|
||||
// An ErrorResponse reports the error caused by an API request
|
||||
type ErrorResponse struct {
|
||||
// HTTP response that caused this error
|
||||
Response *http.Response
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
|
||||
// RequestID returned from the API, useful to contact support.
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
// Rate contains the rate limit for the current client.
|
||||
type Rate struct {
|
||||
// The number of request per hour the client is currently limited to.
|
||||
Limit int `json:"limit"`
|
||||
|
||||
// The number of remaining requests the client can make this hour.
|
||||
Remaining int `json:"remaining"`
|
||||
|
||||
// The time at which the current rate limit will reset.
|
||||
Reset Timestamp `json:"reset"`
|
||||
}
|
||||
|
||||
func addOptions(s string, opt interface{}) (string, error) {
|
||||
v := reflect.ValueOf(opt)
|
||||
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
origURL, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
origValues := origURL.Query()
|
||||
|
||||
newValues, err := query.Values(opt)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
for k, v := range newValues {
|
||||
origValues[k] = v
|
||||
}
|
||||
|
||||
origURL.RawQuery = origValues.Encode()
|
||||
return origURL.String(), nil
|
||||
}
|
||||
|
||||
// NewClient returns a new DigitalOcean API client.
|
||||
func NewClient(httpClient *http.Client) *Client {
|
||||
if httpClient == nil {
|
||||
httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
|
||||
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
|
||||
c.Account = &AccountServiceOp{client: c}
|
||||
c.Actions = &ActionsServiceOp{client: c}
|
||||
c.Domains = &DomainsServiceOp{client: c}
|
||||
c.Droplets = &DropletsServiceOp{client: c}
|
||||
c.DropletActions = &DropletActionsServiceOp{client: c}
|
||||
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
|
||||
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
|
||||
c.Images = &ImagesServiceOp{client: c}
|
||||
c.ImageActions = &ImageActionsServiceOp{client: c}
|
||||
c.Keys = &KeysServiceOp{client: c}
|
||||
c.Regions = &RegionsServiceOp{client: c}
|
||||
c.Snapshots = &SnapshotsServiceOp{client: c}
|
||||
c.Sizes = &SizesServiceOp{client: c}
|
||||
c.Storage = &StorageServiceOp{client: c}
|
||||
c.StorageActions = &StorageActionsServiceOp{client: c}
|
||||
c.Tags = &TagsServiceOp{client: c}
|
||||
c.LoadBalancers = &LoadBalancersServiceOp{client: c}
|
||||
c.Certificates = &CertificatesServiceOp{client: c}
|
||||
c.Firewalls = &FirewallsServiceOp{client: c}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// ClientOpt are options for New.
|
||||
type ClientOpt func(*Client) error
|
||||
|
||||
// New returns a new DIgitalOcean API client instance.
|
||||
func New(httpClient *http.Client, opts ...ClientOpt) (*Client, error) {
|
||||
c := NewClient(httpClient)
|
||||
for _, opt := range opts {
|
||||
if err := opt(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SetBaseURL is a client option for setting the base URL.
|
||||
func SetBaseURL(bu string) ClientOpt {
|
||||
return func(c *Client) error {
|
||||
u, err := url.Parse(bu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.BaseURL = u
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserAgent is a client option for setting the user agent.
|
||||
func SetUserAgent(ua string) ClientOpt {
|
||||
return func(c *Client) error {
|
||||
c.UserAgent = fmt.Sprintf("%s+%s", ua, c.UserAgent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewRequest creates an API request. A relative URL can be provided in urlStr, which will be resolved to the
|
||||
// BaseURL of the Client. Relative URLS should always be specified without a preceding slash. If specified, the
|
||||
// value pointed to by body is JSON encoded and included in as the request body.
|
||||
func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) {
|
||||
rel, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := c.BaseURL.ResolveReference(rel)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", mediaType)
|
||||
req.Header.Add("Accept", mediaType)
|
||||
req.Header.Add("User-Agent", c.UserAgent)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// OnRequestCompleted sets the DO API request completion callback
|
||||
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
|
||||
c.onRequestCompleted = rc
|
||||
}
|
||||
|
||||
// newResponse creates a new Response for the provided http.Response
|
||||
func newResponse(r *http.Response) *Response {
|
||||
response := Response{Response: r}
|
||||
response.populateRate()
|
||||
|
||||
return &response
|
||||
}
|
||||
|
||||
func (r *Response) links() (map[string]headerLink.Link, error) {
|
||||
if linkText, ok := r.Response.Header["Link"]; ok {
|
||||
links, err := headerLink.Parse(linkText[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
linkMap := map[string]headerLink.Link{}
|
||||
for _, link := range links {
|
||||
linkMap[link.Rel] = link
|
||||
}
|
||||
|
||||
return linkMap, nil
|
||||
}
|
||||
|
||||
return map[string]headerLink.Link{}, nil
|
||||
}
|
||||
|
||||
// populateRate parses the rate related headers and populates the response Rate.
|
||||
func (r *Response) populateRate() {
|
||||
if limit := r.Header.Get(headerRateLimit); limit != "" {
|
||||
r.Rate.Limit, _ = strconv.Atoi(limit)
|
||||
}
|
||||
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
|
||||
r.Rate.Remaining, _ = strconv.Atoi(remaining)
|
||||
}
|
||||
if reset := r.Header.Get(headerRateReset); reset != "" {
|
||||
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
|
||||
r.Rate.Reset = Timestamp{time.Unix(v, 0)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
|
||||
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
|
||||
// the raw response will be written to v, without attempting to decode it.
|
||||
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
|
||||
resp, err := context.DoRequestWithClient(ctx, c.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.onRequestCompleted != nil {
|
||||
c.onRequestCompleted(req, resp)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if rerr := resp.Body.Close(); err == nil {
|
||||
err = rerr
|
||||
}
|
||||
}()
|
||||
|
||||
response := newResponse(resp)
|
||||
c.Rate = response.Rate
|
||||
|
||||
err = CheckResponse(resp)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
if w, ok := v.(io.Writer); ok {
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(resp.Body).Decode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
func (r *ErrorResponse) Error() string {
|
||||
if r.RequestID != "" {
|
||||
return fmt.Sprintf("%v %v: %d (request %q) %v",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
|
||||
}
|
||||
return fmt.Sprintf("%v %v: %d %v",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
|
||||
}
|
||||
|
||||
// CheckResponse checks the API response for errors, and returns them if present. A response is considered an
|
||||
// error if it has a status code outside the 200 range. API error responses are expected to have either no response
|
||||
// body, or a JSON response body that maps to ErrorResponse. Any other response body will be silently ignored.
|
||||
func CheckResponse(r *http.Response) error {
|
||||
if c := r.StatusCode; c >= 200 && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
err := json.Unmarshal(data, errorResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
func (r Rate) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// String is a helper routine that allocates a new string value
|
||||
// to store v and returns a pointer to it.
|
||||
func String(v string) *string {
|
||||
p := new(string)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Int is a helper routine that allocates a new int32 value
|
||||
// to store v and returns a pointer to it, but unlike Int32
|
||||
// its argument value is an int.
|
||||
func Int(v int) *int {
|
||||
p := new(int)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Bool is a helper routine that allocates a new bool value
|
||||
// to store v and returns a pointer to it.
|
||||
func Bool(v bool) *bool {
|
||||
p := new(bool)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// StreamToString converts a reader to a string
|
||||
func StreamToString(stream io.Reader) string {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(stream)
|
||||
return buf.String()
|
||||
}
|
|
@ -0,0 +1,540 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
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",
|
||||
"Domains",
|
||||
"Droplets",
|
||||
"DropletActions",
|
||||
"Images",
|
||||
"ImageActions",
|
||||
"Keys",
|
||||
"Regions",
|
||||
"Sizes",
|
||||
"FloatingIPs",
|
||||
"FloatingIPActions",
|
||||
"Tags",
|
||||
}
|
||||
|
||||
cp := reflect.ValueOf(c)
|
||||
cv := reflect.Indirect(cp)
|
||||
|
||||
for _, s := range services {
|
||||
if cv.FieldByName(s).IsNil() {
|
||||
t.Errorf("c.%s shouldn't be nil", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaultBaseURL(t *testing.T, c *Client) {
|
||||
if c.BaseURL == nil || c.BaseURL.String() != defaultBaseURL {
|
||||
t.Errorf("NewClient BaseURL = %v, expected %v", c.BaseURL, defaultBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaultUserAgent(t *testing.T, c *Client) {
|
||||
if c.UserAgent != userAgent {
|
||||
t.Errorf("NewClick UserAgent = %v, expected %v", c.UserAgent, userAgent)
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaults(t *testing.T, c *Client) {
|
||||
testClientDefaultBaseURL(t, c)
|
||||
testClientDefaultUserAgent(t, c)
|
||||
testClientServices(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
c := NewClient(nil)
|
||||
testClientDefaults(t, c)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c, err := New(nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("New(): %v", err)
|
||||
}
|
||||
testClientDefaults(t, c)
|
||||
}
|
||||
|
||||
func TestNewRequest(t *testing.T) {
|
||||
c := NewClient(nil)
|
||||
|
||||
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"
|
||||
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) {
|
||||
c, err := New(nil, SetUserAgent("testing"))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("New() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := fmt.Sprintf("%s+%s", "testing", userAgent)
|
||||
if got := c.UserAgent; got != expected {
|
||||
t.Errorf("New() UserAgent = %s; expected %s", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomBaseURL(t *testing.T) {
|
||||
baseURL := "http://localhost/foo"
|
||||
c, err := New(nil, SetBaseURL(baseURL))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("New() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := baseURL
|
||||
if got := c.BaseURL.String(); got != expected {
|
||||
t.Errorf("New() BaseURL = %s; expected %s", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomBaseURL_badURL(t *testing.T) {
|
||||
baseURL := ":"
|
||||
_, err := New(nil, SetBaseURL(baseURL))
|
||||
|
||||
testURLParseError(t, err)
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// ImageActionsService is an interface for interfacing with the image actions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#image-actions
|
||||
type ImageActionsService interface {
|
||||
Get(context.Context, int, int) (*Action, *Response, error)
|
||||
Transfer(context.Context, int, *ActionRequest) (*Action, *Response, error)
|
||||
Convert(context.Context, int) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// ImageActionsServiceOp handles communition with the image action related methods of the
|
||||
// DigitalOcean API.
|
||||
type ImageActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ImageActionsService = &ImageActionsServiceOp{}
|
||||
|
||||
// Transfer an image
|
||||
func (i *ImageActionsServiceOp) Transfer(ctx context.Context, imageID int, transferRequest *ActionRequest) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if transferRequest == nil {
|
||||
return nil, nil, NewArgError("transferRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions", imageID)
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodPost, path, transferRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
// Convert an image to a snapshot
|
||||
func (i *ImageActionsServiceOp) Convert(ctx context.Context, imageID int) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannont be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions", imageID)
|
||||
|
||||
convertRequest := &ActionRequest{
|
||||
"type": "convert",
|
||||
}
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodPost, path, convertRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
// Get an action for a particular image by id.
|
||||
func (i *ImageActionsServiceOp) Get(ctx context.Context, imageID, actionID int) (*Action, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if actionID < 1 {
|
||||
return nil, nil, NewArgError("actionID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
|
||||
|
||||
req, err := i.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := i.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const imageBasePath = "v2/images"
|
||||
|
||||
// ImagesService is an interface for interfacing with the images
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#images
|
||||
type ImagesService interface {
|
||||
List(context.Context, *ListOptions) ([]Image, *Response, error)
|
||||
ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
|
||||
GetByID(context.Context, int) (*Image, *Response, error)
|
||||
GetBySlug(context.Context, string) (*Image, *Response, error)
|
||||
Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error)
|
||||
Delete(context.Context, int) (*Response, error)
|
||||
}
|
||||
|
||||
// ImagesServiceOp handles communication with the image related methods of the
|
||||
// DigitalOcean API.
|
||||
type ImagesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ImagesService = &ImagesServiceOp{}
|
||||
|
||||
// Image represents a DigitalOcean Image
|
||||
type Image struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Distribution string `json:"distribution,omitempty"`
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
MinDiskSize int `json:"min_disk_size,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// ImageUpdateRequest represents a request to update an image.
|
||||
type ImageUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type imageRoot struct {
|
||||
Image *Image
|
||||
}
|
||||
|
||||
type imagesRoot struct {
|
||||
Images []Image
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type listImageOptions struct {
|
||||
Private bool `url:"private,omitempty"`
|
||||
Type string `url:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (i Image) String() string {
|
||||
return Stringify(i)
|
||||
}
|
||||
|
||||
// List lists all the images available.
|
||||
func (s *ImagesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
return s.list(ctx, opt, nil)
|
||||
}
|
||||
|
||||
// ListDistribution lists all the distribution images.
|
||||
func (s *ImagesServiceOp) ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Type: "distribution"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListApplication lists all the application images.
|
||||
func (s *ImagesServiceOp) ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Type: "application"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListUser lists all the user images.
|
||||
func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error) {
|
||||
listOpt := listImageOptions{Private: true}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// GetByID retrieves an image by id.
|
||||
func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
return s.get(ctx, interface{}(imageID))
|
||||
}
|
||||
|
||||
// GetBySlug retrieves an image by slug.
|
||||
func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, *Response, error) {
|
||||
if len(slug) < 1 {
|
||||
return nil, nil, NewArgError("slug", "cannot be blank")
|
||||
}
|
||||
|
||||
return s.get(ctx, interface{}(slug))
|
||||
}
|
||||
|
||||
// Update an image name.
|
||||
func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imageRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Image, resp, err
|
||||
}
|
||||
|
||||
// Delete an image.
|
||||
func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, error) {
|
||||
if imageID < 1 {
|
||||
return nil, NewArgError("imageID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Helper method for getting an individual image
|
||||
func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%v", imageBasePath, ID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imageRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Image, resp, err
|
||||
}
|
||||
|
||||
// Helper method for listing images
|
||||
func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listImageOptions) ([]Image, *Response, error) {
|
||||
path := imageBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
path, err = addOptions(path, listOpt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(imagesRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Images, resp, err
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
package 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}]}`)
|
||||
})
|
||||
|
||||
images, _, err := client.Images.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Images.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(images, expected) {
|
||||
t.Errorf("Images.List returned %+v, expected %+v", images, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
images, _, err := client.Images.ListDistribution(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Images.ListDistribution returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(images, expected) {
|
||||
t.Errorf("Images.ListDistribution returned %+v, expected %+v", images, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
images, _, err := client.Images.ListApplication(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Images.ListApplication returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(images, expected) {
|
||||
t.Errorf("Images.ListApplication returned %+v, expected %+v", images, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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}]}`)
|
||||
})
|
||||
|
||||
images, _, err := client.Images.ListUser(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Images.ListUser returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Image{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(images, expected) {
|
||||
t.Errorf("Images.ListUser returned %+v, expected %+v", images, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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_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,
|
||||
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, Created:"2013-11-27T09:24:55Z"}`
|
||||
if expected != stringified {
|
||||
t.Errorf("Image.String returned %+v, expected %+v", stringified, expected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const keysBasePath = "v2/account/keys"
|
||||
|
||||
// KeysService is an interface for interfacing with the keys
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#keys
|
||||
type KeysService interface {
|
||||
List(context.Context, *ListOptions) ([]Key, *Response, error)
|
||||
GetByID(context.Context, int) (*Key, *Response, error)
|
||||
GetByFingerprint(context.Context, string) (*Key, *Response, error)
|
||||
Create(context.Context, *KeyCreateRequest) (*Key, *Response, error)
|
||||
UpdateByID(context.Context, int, *KeyUpdateRequest) (*Key, *Response, error)
|
||||
UpdateByFingerprint(context.Context, string, *KeyUpdateRequest) (*Key, *Response, error)
|
||||
DeleteByID(context.Context, int) (*Response, error)
|
||||
DeleteByFingerprint(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// KeysServiceOp handles communication with key related method of the
|
||||
// DigitalOcean API.
|
||||
type KeysServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ KeysService = &KeysServiceOp{}
|
||||
|
||||
// Key represents a DigitalOcean Key.
|
||||
type Key struct {
|
||||
ID int `json:"id,float64,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
PublicKey string `json:"public_key,omitempty"`
|
||||
}
|
||||
|
||||
// KeyUpdateRequest represents a request to update a DigitalOcean key.
|
||||
type KeyUpdateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type keysRoot struct {
|
||||
SSHKeys []Key `json:"ssh_keys"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type keyRoot struct {
|
||||
SSHKey *Key `json:"ssh_key"`
|
||||
}
|
||||
|
||||
func (s Key) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// KeyCreateRequest represents a request to create a new key.
|
||||
type KeyCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
PublicKey string `json:"public_key"`
|
||||
}
|
||||
|
||||
// List all keys
|
||||
func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Response, error) {
|
||||
path := keysBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keysRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.SSHKeys, resp, err
|
||||
}
|
||||
|
||||
// Performs a get given a path
|
||||
func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// GetByID gets a Key by id
|
||||
func (s *KeysServiceOp) GetByID(ctx context.Context, keyID int) (*Key, *Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// GetByFingerprint gets a Key by by fingerprint
|
||||
func (s *KeysServiceOp) GetByFingerprint(ctx context.Context, fingerprint string) (*Key, *Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, nil, NewArgError("fingerprint", "cannot not be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// Create a key using a KeyCreateRequest
|
||||
func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequest) (*Key, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, keysBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// UpdateByID updates a key name by ID.
|
||||
func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// UpdateByFingerprint updates a key name by fingerprint.
|
||||
func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint string, updateRequest *KeyUpdateRequest) (*Key, *Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, nil, NewArgError("fingerprint", "cannot be empty")
|
||||
}
|
||||
|
||||
if updateRequest == nil {
|
||||
return nil, nil, NewArgError("updateRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(keyRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.SSHKey, resp, err
|
||||
}
|
||||
|
||||
// Delete key using a path
|
||||
func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// DeleteByID deletes a key by its id
|
||||
func (s *KeysServiceOp) DeleteByID(ctx context.Context, keyID int) (*Response, error) {
|
||||
if keyID < 1 {
|
||||
return nil, NewArgError("keyID", "cannot be less than 1")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%d", keysBasePath, keyID)
|
||||
return s.delete(ctx, path)
|
||||
}
|
||||
|
||||
// DeleteByFingerprint deletes a key by its fingerprint
|
||||
func (s *KeysServiceOp) DeleteByFingerprint(ctx context.Context, fingerprint string) (*Response, error) {
|
||||
if len(fingerprint) < 1 {
|
||||
return nil, NewArgError("fingerprint", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", keysBasePath, fingerprint)
|
||||
return s.delete(ctx, path)
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
package 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}]}`)
|
||||
})
|
||||
|
||||
keys, _, err := client.Keys.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Keys.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Key{{ID: 1}, {ID: 2}}
|
||||
if !reflect.DeepEqual(keys, expected) {
|
||||
t.Errorf("Keys.List returned %+v, expected %+v", keys, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// Links manages links that are returned along with a List
|
||||
type Links struct {
|
||||
Pages *Pages `json:"pages,omitempty"`
|
||||
Actions []LinkAction `json:"actions,omitempty"`
|
||||
}
|
||||
|
||||
// Pages are pages specified in Links
|
||||
type Pages struct {
|
||||
First string `json:"first,omitempty"`
|
||||
Prev string `json:"prev,omitempty"`
|
||||
Last string `json:"last,omitempty"`
|
||||
Next string `json:"next,omitempty"`
|
||||
}
|
||||
|
||||
// LinkAction is a pointer to an action
|
||||
type LinkAction struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Rel string `json:"rel,omitempty"`
|
||||
HREF string `json:"href,omitempty"`
|
||||
}
|
||||
|
||||
// CurrentPage is current page of the list
|
||||
func (l *Links) CurrentPage() (int, error) {
|
||||
return l.Pages.current()
|
||||
}
|
||||
|
||||
func (p *Pages) current() (int, error) {
|
||||
switch {
|
||||
case p == nil:
|
||||
return 1, nil
|
||||
case p.Prev == "" && p.Next != "":
|
||||
return 1, nil
|
||||
case p.Prev != "":
|
||||
prevPage, err := pageForURL(p.Prev)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return prevPage + 1, nil
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// IsLastPage returns true if the current page is the last
|
||||
func (l *Links) IsLastPage() bool {
|
||||
if l.Pages == nil {
|
||||
return true
|
||||
}
|
||||
return l.Pages.isLast()
|
||||
}
|
||||
|
||||
func (p *Pages) isLast() bool {
|
||||
return p.Last == ""
|
||||
}
|
||||
|
||||
func pageForURL(urlText string) (int, error) {
|
||||
u, err := url.ParseRequestURI(urlText)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
pageStr := u.Query().Get("page")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// Get a link action by id.
|
||||
func (la *LinkAction) Get(ctx context.Context, client *Client) (*Action, *Response, error) {
|
||||
return client.Actions.Get(ctx, la.ID)
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
package 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"
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
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_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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const loadBalancersBasePath = "/v2/load_balancers"
|
||||
const forwardingRulesPath = "forwarding_rules"
|
||||
|
||||
const dropletsPath = "droplets"
|
||||
|
||||
// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#load-balancers
|
||||
type LoadBalancersService interface {
|
||||
Get(context.Context, string) (*LoadBalancer, *Response, error)
|
||||
List(context.Context, *ListOptions) ([]LoadBalancer, *Response, error)
|
||||
Create(context.Context, *LoadBalancerRequest) (*LoadBalancer, *Response, error)
|
||||
Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error)
|
||||
Delete(ctx context.Context, lbID string) (*Response, error)
|
||||
AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
|
||||
RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error)
|
||||
AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
|
||||
RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error)
|
||||
}
|
||||
|
||||
// LoadBalancer represents a DigitalOcean load balancer configuration.
|
||||
type LoadBalancer struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
|
||||
Region *Region `json:"region,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a LoadBalancer.
|
||||
func (l LoadBalancer) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
// ForwardingRule represents load balancer forwarding rules.
|
||||
type ForwardingRule struct {
|
||||
EntryProtocol string `json:"entry_protocol,omitempty"`
|
||||
EntryPort int `json:"entry_port,omitempty"`
|
||||
TargetProtocol string `json:"target_protocol,omitempty"`
|
||||
TargetPort int `json:"target_port,omitempty"`
|
||||
CertificateID string `json:"certificate_id,omitempty"`
|
||||
TlsPassthrough bool `json:"tls_passthrough,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a ForwardingRule.
|
||||
func (f ForwardingRule) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
// HealthCheck represents optional load balancer health check rules.
|
||||
type HealthCheck struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
CheckIntervalSeconds int `json:"check_interval_seconds,omitempty"`
|
||||
ResponseTimeoutSeconds int `json:"response_timeout_seconds,omitempty"`
|
||||
HealthyThreshold int `json:"healthy_threshold,omitempty"`
|
||||
UnhealthyThreshold int `json:"unhealthy_threshold,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a HealthCheck.
|
||||
func (h HealthCheck) String() string {
|
||||
return Stringify(h)
|
||||
}
|
||||
|
||||
// StickySessions represents optional load balancer session affinity rules.
|
||||
type StickySessions struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
CookieName string `json:"cookie_name,omitempty"`
|
||||
CookieTtlSeconds int `json:"cookie_ttl_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a StickySessions instance.
|
||||
func (s StickySessions) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// LoadBalancerRequest represents the configuration to be applied to an existing or a new load balancer.
|
||||
type LoadBalancerRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
ForwardingRules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
StickySessions *StickySessions `json:"sticky_sessions,omitempty"`
|
||||
DropletIDs []int `json:"droplet_ids,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
RedirectHttpToHttps bool `json:"redirect_http_to_https,omitempty"`
|
||||
}
|
||||
|
||||
// String creates a human-readable description of a LoadBalancerRequest.
|
||||
func (l LoadBalancerRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type forwardingRulesRequest struct {
|
||||
Rules []ForwardingRule `json:"forwarding_rules,omitempty"`
|
||||
}
|
||||
|
||||
func (l forwardingRulesRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type dropletIDsRequest struct {
|
||||
IDs []int `json:"droplet_ids,omitempty"`
|
||||
}
|
||||
|
||||
func (l dropletIDsRequest) String() string {
|
||||
return Stringify(l)
|
||||
}
|
||||
|
||||
type loadBalancersRoot struct {
|
||||
LoadBalancers []LoadBalancer `json:"load_balancers"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type loadBalancerRoot struct {
|
||||
LoadBalancer *LoadBalancer `json:"load_balancer"`
|
||||
}
|
||||
|
||||
// LoadBalancersServiceOp handles communication with load balancer-related methods of the DigitalOcean API.
|
||||
type LoadBalancersServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ LoadBalancersService = &LoadBalancersServiceOp{}
|
||||
|
||||
// Get an existing load balancer by its identifier.
|
||||
func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// List load balancers, with optional pagination.
|
||||
func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]LoadBalancer, *Response, error) {
|
||||
path, err := addOptions(loadBalancersBasePath, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancersRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.LoadBalancers, resp, err
|
||||
}
|
||||
|
||||
// Create a new load balancer with a given configuration.
|
||||
func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// Update an existing load balancer with new configuration.
|
||||
func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, "PUT", path, lbr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(loadBalancerRoot)
|
||||
resp, err := l.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.LoadBalancer, resp, err
|
||||
}
|
||||
|
||||
// Delete a load balancer by its identifier.
|
||||
func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// AddDroplets adds droplets to a load balancer.
|
||||
func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveDroplets removes droplets from a load balancer.
|
||||
func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// AddForwardingRules adds forwarding rules to a load balancer.
|
||||
func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveForwardingRules removes forwarding rules from a load balancer.
|
||||
func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
|
||||
|
||||
req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l.client.Do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,796 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var lbListJSONResponse = `
|
||||
{
|
||||
"load_balancers":[
|
||||
{
|
||||
"id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
|
||||
"name":"example-lb-01",
|
||||
"ip":"46.214.185.203",
|
||||
"algorithm":"round_robin",
|
||||
"status":"active",
|
||||
"created_at":"2016-12-15T14:16:36Z",
|
||||
"forwarding_rules":[
|
||||
{
|
||||
"entry_protocol":"https",
|
||||
"entry_port":443,
|
||||
"target_protocol":"http",
|
||||
"target_port":80,
|
||||
"certificate_id":"a-b-c"
|
||||
}
|
||||
],
|
||||
"health_check":{
|
||||
"protocol":"http",
|
||||
"port":80,
|
||||
"path":"/index.html",
|
||||
"check_interval_seconds":10,
|
||||
"response_timeout_seconds":5,
|
||||
"healthy_threshold":5,
|
||||
"unhealthy_threshold":3
|
||||
},
|
||||
"sticky_sessions":{
|
||||
"type":"cookies",
|
||||
"cookie_name":"DO-LB",
|
||||
"cookie_ttl_seconds":5
|
||||
},
|
||||
"region":{
|
||||
"name":"New York 1",
|
||||
"slug":"nyc1",
|
||||
"sizes":[
|
||||
"512mb",
|
||||
"1gb",
|
||||
"2gb",
|
||||
"4gb",
|
||||
"8gb",
|
||||
"16gb"
|
||||
],
|
||||
"features":[
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"storage"
|
||||
],
|
||||
"available":true
|
||||
},
|
||||
"droplet_ids":[
|
||||
2,
|
||||
21
|
||||
]
|
||||
}
|
||||
],
|
||||
"links":{
|
||||
"pages":{
|
||||
"last":"http://localhost:3001/v2/load_balancers?page=3&per_page=1",
|
||||
"next":"http://localhost:3001/v2/load_balancers?page=2&per_page=1"
|
||||
}
|
||||
},
|
||||
"meta":{
|
||||
"total":3
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var lbCreateJSONResponse = `
|
||||
{
|
||||
"load_balancer":{
|
||||
"id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
|
||||
"name":"example-lb-01",
|
||||
"ip":"",
|
||||
"algorithm":"round_robin",
|
||||
"status":"new",
|
||||
"created_at":"2016-12-15T14:19:09Z",
|
||||
"forwarding_rules":[
|
||||
{
|
||||
"entry_protocol":"https",
|
||||
"entry_port":443,
|
||||
"target_protocol":"http",
|
||||
"target_port":80,
|
||||
"certificate_id":"a-b-c"
|
||||
},
|
||||
{
|
||||
"entry_protocol":"https",
|
||||
"entry_port":444,
|
||||
"target_protocol":"https",
|
||||
"target_port":443,
|
||||
"tls_passthrough":true
|
||||
}
|
||||
],
|
||||
"health_check":{
|
||||
"protocol":"http",
|
||||
"port":80,
|
||||
"path":"/index.html",
|
||||
"check_interval_seconds":10,
|
||||
"response_timeout_seconds":5,
|
||||
"healthy_threshold":5,
|
||||
"unhealthy_threshold":3
|
||||
},
|
||||
"sticky_sessions":{
|
||||
"type":"cookies",
|
||||
"cookie_name":"DO-LB",
|
||||
"cookie_ttl_seconds":5
|
||||
},
|
||||
"region":{
|
||||
"name":"New York 1",
|
||||
"slug":"nyc1",
|
||||
"sizes":[
|
||||
"512mb",
|
||||
"1gb",
|
||||
"2gb",
|
||||
"4gb",
|
||||
"8gb",
|
||||
"16gb"
|
||||
],
|
||||
"features":[
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"storage"
|
||||
],
|
||||
"available":true
|
||||
},
|
||||
"droplet_ids":[
|
||||
2,
|
||||
21
|
||||
],
|
||||
"redirect_http_to_https":true
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var lbGetJSONResponse = `
|
||||
{
|
||||
"load_balancer":{
|
||||
"id":"37e6be88-01ec-4ec7-9bc6-a514d4719057",
|
||||
"name":"example-lb-01",
|
||||
"ip":"46.214.185.203",
|
||||
"algorithm":"round_robin",
|
||||
"status":"active",
|
||||
"created_at":"2016-12-15T14:16:36Z",
|
||||
"forwarding_rules":[
|
||||
{
|
||||
"entry_protocol":"https",
|
||||
"entry_port":443,
|
||||
"target_protocol":"http",
|
||||
"target_port":80,
|
||||
"certificate_id":"a-b-c"
|
||||
}
|
||||
],
|
||||
"health_check":{
|
||||
"protocol":"http",
|
||||
"port":80,
|
||||
"path":"/index.html",
|
||||
"check_interval_seconds":10,
|
||||
"response_timeout_seconds":5,
|
||||
"healthy_threshold":5,
|
||||
"unhealthy_threshold":3
|
||||
},
|
||||
"sticky_sessions":{
|
||||
"type":"cookies",
|
||||
"cookie_name":"DO-LB",
|
||||
"cookie_ttl_seconds":5
|
||||
},
|
||||
"region":{
|
||||
"name":"New York 1",
|
||||
"slug":"nyc1",
|
||||
"sizes":[
|
||||
"512mb",
|
||||
"1gb",
|
||||
"2gb",
|
||||
"4gb",
|
||||
"8gb",
|
||||
"16gb"
|
||||
],
|
||||
"features":[
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"storage"
|
||||
],
|
||||
"available":true
|
||||
},
|
||||
"droplet_ids":[
|
||||
2,
|
||||
21
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var lbUpdateJSONResponse = `
|
||||
{
|
||||
"load_balancer":{
|
||||
"id":"8268a81c-fcf5-423e-a337-bbfe95817f23",
|
||||
"name":"example-lb-01",
|
||||
"ip":"12.34.56.78",
|
||||
"algorithm":"least_connections",
|
||||
"status":"active",
|
||||
"created_at":"2016-12-15T14:19:09Z",
|
||||
"forwarding_rules":[
|
||||
{
|
||||
"entry_protocol":"http",
|
||||
"entry_port":80,
|
||||
"target_protocol":"http",
|
||||
"target_port":80
|
||||
},
|
||||
{
|
||||
"entry_protocol":"https",
|
||||
"entry_port":443,
|
||||
"target_protocol":"http",
|
||||
"target_port":80,
|
||||
"certificate_id":"a-b-c"
|
||||
}
|
||||
],
|
||||
"health_check":{
|
||||
"protocol":"tcp",
|
||||
"port":80,
|
||||
"path":"",
|
||||
"check_interval_seconds":10,
|
||||
"response_timeout_seconds":5,
|
||||
"healthy_threshold":5,
|
||||
"unhealthy_threshold":3
|
||||
},
|
||||
"sticky_sessions":{
|
||||
"type":"none"
|
||||
},
|
||||
"region":{
|
||||
"name":"New York 1",
|
||||
"slug":"nyc1",
|
||||
"sizes":[
|
||||
"512mb",
|
||||
"1gb",
|
||||
"2gb",
|
||||
"4gb",
|
||||
"8gb",
|
||||
"16gb"
|
||||
],
|
||||
"features":[
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6",
|
||||
"metadata",
|
||||
"storage"
|
||||
],
|
||||
"available":true
|
||||
},
|
||||
"droplet_ids":[
|
||||
2,
|
||||
21
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestLoadBlanacers_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
path := "/v2/load_balancers"
|
||||
loadBalancerId := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path = fmt.Sprintf("%s/%s", path, loadBalancerId)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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 TestLoadBlanacers_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &LoadBalancerRequest{
|
||||
Name: "example-lb-01",
|
||||
Algorithm: "round_robin",
|
||||
Region: "nyc1",
|
||||
ForwardingRules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 443,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
CertificateID: "a-b-c",
|
||||
},
|
||||
},
|
||||
HealthCheck: &HealthCheck{
|
||||
Protocol: "http",
|
||||
Port: 80,
|
||||
Path: "/index.html",
|
||||
CheckIntervalSeconds: 10,
|
||||
ResponseTimeoutSeconds: 5,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 5,
|
||||
},
|
||||
StickySessions: &StickySessions{
|
||||
Type: "cookies",
|
||||
CookieName: "DO-LB",
|
||||
CookieTtlSeconds: 5,
|
||||
},
|
||||
Tag: "my-tag",
|
||||
DropletIDs: []int{2, 21},
|
||||
RedirectHttpToHttps: true,
|
||||
}
|
||||
|
||||
path := "/v2/load_balancers"
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(LoadBalancerRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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"},
|
||||
},
|
||||
DropletIDs: []int{2, 21},
|
||||
RedirectHttpToHttps: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, loadBalancer)
|
||||
}
|
||||
|
||||
func TestLoadBlanacers_Update(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
updateRequest := &LoadBalancerRequest{
|
||||
Name: "example-lb-01",
|
||||
Algorithm: "least_connections",
|
||||
Region: "nyc1",
|
||||
ForwardingRules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "http",
|
||||
EntryPort: 80,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
},
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 443,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
CertificateID: "a-b-c",
|
||||
},
|
||||
},
|
||||
HealthCheck: &HealthCheck{
|
||||
Protocol: "tcp",
|
||||
Port: 80,
|
||||
Path: "",
|
||||
CheckIntervalSeconds: 10,
|
||||
ResponseTimeoutSeconds: 5,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 5,
|
||||
},
|
||||
StickySessions: &StickySessions{
|
||||
Type: "none",
|
||||
},
|
||||
DropletIDs: []int{2, 21},
|
||||
}
|
||||
|
||||
path := "/v2/load_balancers"
|
||||
loadBalancerId := "8268a81c-fcf5-423e-a337-bbfe95817f23"
|
||||
path = fmt.Sprintf("%s/%s", path, loadBalancerId)
|
||||
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(LoadBalancerRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, "PUT")
|
||||
assert.Equal(t, updateRequest, v)
|
||||
|
||||
fmt.Fprint(w, lbUpdateJSONResponse)
|
||||
})
|
||||
|
||||
loadBalancer, _, err := client.LoadBalancers.Update(ctx, loadBalancerId, updateRequest)
|
||||
if err != nil {
|
||||
t.Errorf("LoadBalancers.Update returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := &LoadBalancer{
|
||||
ID: "8268a81c-fcf5-423e-a337-bbfe95817f23",
|
||||
Name: "example-lb-01",
|
||||
IP: "12.34.56.78",
|
||||
Algorithm: "least_connections",
|
||||
Status: "active",
|
||||
Created: "2016-12-15T14:19:09Z",
|
||||
ForwardingRules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "http",
|
||||
EntryPort: 80,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
},
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 443,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
CertificateID: "a-b-c",
|
||||
},
|
||||
},
|
||||
HealthCheck: &HealthCheck{
|
||||
Protocol: "tcp",
|
||||
Port: 80,
|
||||
Path: "",
|
||||
CheckIntervalSeconds: 10,
|
||||
ResponseTimeoutSeconds: 5,
|
||||
UnhealthyThreshold: 3,
|
||||
HealthyThreshold: 5,
|
||||
},
|
||||
StickySessions: &StickySessions{
|
||||
Type: "none",
|
||||
},
|
||||
Region: &Region{
|
||||
Slug: "nyc1",
|
||||
Name: "New York 1",
|
||||
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
|
||||
Available: true,
|
||||
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
|
||||
},
|
||||
DropletIDs: []int{2, 21},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, loadBalancer)
|
||||
}
|
||||
|
||||
func TestLoadBlanacers_List(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
path := "/v2/load_balancers"
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, lbListJSONResponse)
|
||||
})
|
||||
|
||||
loadBalancers, _, err := client.LoadBalancers.List(ctx, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("LoadBalancers.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []LoadBalancer{
|
||||
{
|
||||
ID: "37e6be88-01ec-4ec7-9bc6-a514d4719057",
|
||||
Name: "example-lb-01",
|
||||
IP: "46.214.185.203",
|
||||
Algorithm: "round_robin",
|
||||
Status: "active",
|
||||
Created: "2016-12-15T14:16:36Z",
|
||||
ForwardingRules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 443,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 80,
|
||||
CertificateID: "a-b-c",
|
||||
},
|
||||
},
|
||||
HealthCheck: &HealthCheck{
|
||||
Protocol: "http",
|
||||
Port: 80,
|
||||
Path: "/index.html",
|
||||
CheckIntervalSeconds: 10,
|
||||
ResponseTimeoutSeconds: 5,
|
||||
HealthyThreshold: 5,
|
||||
UnhealthyThreshold: 3,
|
||||
},
|
||||
StickySessions: &StickySessions{
|
||||
Type: "cookies",
|
||||
CookieName: "DO-LB",
|
||||
CookieTtlSeconds: 5,
|
||||
},
|
||||
Region: &Region{
|
||||
Slug: "nyc1",
|
||||
Name: "New York 1",
|
||||
Sizes: []string{"512mb", "1gb", "2gb", "4gb", "8gb", "16gb"},
|
||||
Available: true,
|
||||
Features: []string{"private_networking", "backups", "ipv6", "metadata", "storage"},
|
||||
},
|
||||
DropletIDs: []int{2, 21},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, loadBalancers)
|
||||
}
|
||||
|
||||
func TestLoadBlanacers_List_Pagination(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
path := "/v2/load_balancers"
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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 TestLoadBlanacers_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path := "/v2/load_balancers"
|
||||
path = fmt.Sprintf("%s/%s", path, lbID)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodDelete)
|
||||
})
|
||||
|
||||
_, err := client.LoadBalancers.Delete(ctx, lbID)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("LoadBalancers.Delete returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBlanacers_AddDroplets(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
dropletIdsRequest := &dropletIDsRequest{
|
||||
IDs: []int{42, 44},
|
||||
}
|
||||
|
||||
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(dropletIDsRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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 TestLoadBlanacers_RemoveDroplets(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
dropletIdsRequest := &dropletIDsRequest{
|
||||
IDs: []int{2, 21},
|
||||
}
|
||||
|
||||
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path := fmt.Sprintf("/v2/load_balancers/%s/droplets", lbID)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(dropletIDsRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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 TestLoadBlanacers_AddForwardingRules(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
frr := &forwardingRulesRequest{
|
||||
Rules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 444,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 81,
|
||||
CertificateID: "b2abc00f-d3c4-426c-9f0b-b2f7a3ff7527",
|
||||
},
|
||||
{
|
||||
EntryProtocol: "tcp",
|
||||
EntryPort: 8080,
|
||||
TargetProtocol: "tcp",
|
||||
TargetPort: 8081,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(forwardingRulesRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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 TestLoadBlanacers_RemoveForwardingRules(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
frr := &forwardingRulesRequest{
|
||||
Rules: []ForwardingRule{
|
||||
{
|
||||
EntryProtocol: "https",
|
||||
EntryPort: 444,
|
||||
TargetProtocol: "http",
|
||||
TargetPort: 81,
|
||||
},
|
||||
{
|
||||
EntryProtocol: "tcp",
|
||||
EntryPort: 8080,
|
||||
TargetProtocol: "tcp",
|
||||
TargetPort: 8081,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
lbID := "37e6be88-01ec-4ec7-9bc6-a514d4719057"
|
||||
path := fmt.Sprintf("/v2/load_balancers/%s/forwarding_rules", lbID)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(forwardingRulesRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// RegionsService is an interface for interfacing with the regions
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#regions
|
||||
type RegionsService interface {
|
||||
List(context.Context, *ListOptions) ([]Region, *Response, error)
|
||||
}
|
||||
|
||||
// RegionsServiceOp handles communication with the region related methods of the
|
||||
// DigitalOcean API.
|
||||
type RegionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ RegionsService = &RegionsServiceOp{}
|
||||
|
||||
// Region represents a DigitalOcean Region
|
||||
type Region struct {
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Sizes []string `json:"sizes,omitempty"`
|
||||
Available bool `json:"available,omitempty"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
type regionsRoot struct {
|
||||
Regions []Region
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
func (r Region) String() string {
|
||||
return Stringify(r)
|
||||
}
|
||||
|
||||
// List all regions
|
||||
func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region, *Response, error) {
|
||||
path := "v2/regions"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(regionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Regions, resp, err
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package 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"}]}`)
|
||||
})
|
||||
|
||||
regions, _, err := client.Regions.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Regions.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Region{{Slug: "1"}, {Slug: "2"}}
|
||||
if !reflect.DeepEqual(regions, expected) {
|
||||
t.Errorf("Regions.List returned %+v, expected %+v", regions, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// SizesService is an interface for interfacing with the size
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#sizes
|
||||
type SizesService interface {
|
||||
List(context.Context, *ListOptions) ([]Size, *Response, error)
|
||||
}
|
||||
|
||||
// SizesServiceOp handles communication with the size related methods of the
|
||||
// DigitalOcean API.
|
||||
type SizesServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ SizesService = &SizesServiceOp{}
|
||||
|
||||
// Size represents a DigitalOcean Size
|
||||
type Size struct {
|
||||
Slug string `json:"slug,omitempty"`
|
||||
Memory int `json:"memory,omitempty"`
|
||||
Vcpus int `json:"vcpus,omitempty"`
|
||||
Disk int `json:"disk,omitempty"`
|
||||
PriceMonthly float64 `json:"price_monthly,omitempty"`
|
||||
PriceHourly float64 `json:"price_hourly,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
Available bool `json:"available,omitempty"`
|
||||
Transfer float64 `json:"transfer,omitempty"`
|
||||
}
|
||||
|
||||
func (s Size) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
type sizesRoot struct {
|
||||
Sizes []Size
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
// List all images
|
||||
func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *Response, error) {
|
||||
path := "v2/sizes"
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(sizesRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Sizes, resp, err
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package 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"}]}`)
|
||||
})
|
||||
|
||||
sizes, _, err := client.Sizes.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Sizes.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Size{{Slug: "1"}, {Slug: "2"}}
|
||||
if !reflect.DeepEqual(sizes, expected) {
|
||||
t.Errorf("Sizes.List returned %+v, expected %+v", sizes, expected)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const snapshotBasePath = "v2/snapshots"
|
||||
|
||||
// SnapshotsService is an interface for interfacing with the snapshots
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#snapshots
|
||||
type SnapshotsService interface {
|
||||
List(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
ListVolume(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
ListDroplet(context.Context, *ListOptions) ([]Snapshot, *Response, error)
|
||||
Get(context.Context, string) (*Snapshot, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// SnapshotsServiceOp handles communication with the snapshot related methods of the
|
||||
// DigitalOcean API.
|
||||
type SnapshotsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ SnapshotsService = &SnapshotsServiceOp{}
|
||||
|
||||
// Snapshot represents a DigitalOcean Snapshot
|
||||
type Snapshot struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ResourceType string `json:"resource_type,omitempty"`
|
||||
Regions []string `json:"regions,omitempty"`
|
||||
MinDiskSize int `json:"min_disk_size,omitempty"`
|
||||
SizeGigaBytes float64 `json:"size_gigabytes,omitempty"`
|
||||
Created string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
type snapshotRoot struct {
|
||||
Snapshot *Snapshot `json:"snapshot"`
|
||||
}
|
||||
|
||||
type snapshotsRoot struct {
|
||||
Snapshots []Snapshot `json:"snapshots"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
type listSnapshotOptions struct {
|
||||
ResourceType string `url:"resource_type,omitempty"`
|
||||
}
|
||||
|
||||
func (s Snapshot) String() string {
|
||||
return Stringify(s)
|
||||
}
|
||||
|
||||
// List lists all the snapshots available.
|
||||
func (s *SnapshotsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
return s.list(ctx, opt, nil)
|
||||
}
|
||||
|
||||
// ListDroplet lists all the Droplet snapshots.
|
||||
func (s *SnapshotsServiceOp) ListDroplet(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
listOpt := listSnapshotOptions{ResourceType: "droplet"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// ListVolume lists all the volume snapshots.
|
||||
func (s *SnapshotsServiceOp) ListVolume(ctx context.Context, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
listOpt := listSnapshotOptions{ResourceType: "volume"}
|
||||
return s.list(ctx, opt, &listOpt)
|
||||
}
|
||||
|
||||
// Get retrieves an snapshot by id.
|
||||
func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snapshot, *Response, error) {
|
||||
return s.get(ctx, snapshotID)
|
||||
}
|
||||
|
||||
// Delete an snapshot.
|
||||
func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Helper method for getting an individual snapshot
|
||||
func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", snapshotBasePath, ID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Snapshot, resp, err
|
||||
}
|
||||
|
||||
// Helper method for listing snapshots
|
||||
func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *listSnapshotOptions) ([]Snapshot, *Response, error) {
|
||||
path := snapshotBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
path, err = addOptions(path, listOpt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, err
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
stringified := snapshot.String()
|
||||
expected := `godo.Snapshot{ID:"1", Name:"Snapsh176ot", ResourceID:"0", ResourceType:"droplet", Regions:["one"], MinDiskSize:20, SizeGigaBytes:4.84, Created:"2013-11-27T09:24:55Z"}`
|
||||
if expected != stringified {
|
||||
t.Errorf("Snapshot.String returned %+v, expected %+v", stringified, expected)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const (
|
||||
storageBasePath = "v2"
|
||||
storageAllocPath = storageBasePath + "/volumes"
|
||||
storageSnapPath = storageBasePath + "/snapshots"
|
||||
)
|
||||
|
||||
// StorageService is an interface for interfacing with the storage
|
||||
// endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#storage
|
||||
type StorageService interface {
|
||||
ListVolumes(context.Context, *ListVolumeParams) ([]Volume, *Response, error)
|
||||
GetVolume(context.Context, string) (*Volume, *Response, error)
|
||||
CreateVolume(context.Context, *VolumeCreateRequest) (*Volume, *Response, error)
|
||||
DeleteVolume(context.Context, string) (*Response, error)
|
||||
ListSnapshots(ctx context.Context, volumeID string, opts *ListOptions) ([]Snapshot, *Response, error)
|
||||
GetSnapshot(context.Context, string) (*Snapshot, *Response, error)
|
||||
CreateSnapshot(context.Context, *SnapshotCreateRequest) (*Snapshot, *Response, error)
|
||||
DeleteSnapshot(context.Context, string) (*Response, error)
|
||||
}
|
||||
|
||||
// StorageServiceOp handles communication with the storage volumes related methods of the
|
||||
// DigitalOcean API.
|
||||
type StorageServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ListVolumeParams stores the options you can set for a ListVolumeCall
|
||||
type ListVolumeParams struct {
|
||||
Region string `json:"region"`
|
||||
Name string `json:"name"`
|
||||
ListOptions *ListOptions `json:"list_options,omitempty"`
|
||||
}
|
||||
|
||||
var _ StorageService = &StorageServiceOp{}
|
||||
|
||||
// Volume represents a Digital Ocean block store volume.
|
||||
type Volume struct {
|
||||
ID string `json:"id"`
|
||||
Region *Region `json:"region"`
|
||||
Name string `json:"name"`
|
||||
SizeGigaBytes int64 `json:"size_gigabytes"`
|
||||
Description string `json:"description"`
|
||||
DropletIDs []int `json:"droplet_ids"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (f Volume) String() string {
|
||||
return Stringify(f)
|
||||
}
|
||||
|
||||
type storageVolumesRoot struct {
|
||||
Volumes []Volume `json:"volumes"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type storageVolumeRoot struct {
|
||||
Volume *Volume `json:"volume"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeCreateRequest represents a request to create a block store
|
||||
// volume.
|
||||
type VolumeCreateRequest struct {
|
||||
Region string `json:"region"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
SizeGigaBytes int64 `json:"size_gigabytes"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
}
|
||||
|
||||
// ListVolumes lists all storage volumes.
|
||||
func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolumeParams) ([]Volume, *Response, error) {
|
||||
path := storageAllocPath
|
||||
if params != nil {
|
||||
if params.Region != "" && params.Name != "" {
|
||||
path = fmt.Sprintf("%s?name=%s®ion=%s", path, params.Name, params.Region)
|
||||
}
|
||||
|
||||
if params.ListOptions != nil {
|
||||
var err error
|
||||
path, err = addOptions(path, params.ListOptions)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumesRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Volumes, resp, nil
|
||||
}
|
||||
|
||||
// CreateVolume creates a storage volume. The name must be unique.
|
||||
func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
|
||||
path := storageAllocPath
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumeRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Volume, resp, nil
|
||||
}
|
||||
|
||||
// GetVolume retrieves an individual storage volume.
|
||||
func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(storageVolumeRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Volume, resp, nil
|
||||
}
|
||||
|
||||
// DeleteVolume deletes a storage volume.
|
||||
func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SnapshotCreateRequest represents a request to create a block store
|
||||
// volume.
|
||||
type SnapshotCreateRequest struct {
|
||||
VolumeID string `json:"volume_id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ListSnapshots lists all snapshots related to a storage volume.
|
||||
func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string, opt *ListOptions) ([]Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, volumeID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotsRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Snapshots, resp, nil
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a snapshot of a storage volume.
|
||||
func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Snapshot, resp, nil
|
||||
}
|
||||
|
||||
// GetSnapshot retrieves an individual snapshot.
|
||||
func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(snapshotRoot)
|
||||
resp, err := svc.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Snapshot, resp, nil
|
||||
}
|
||||
|
||||
// DeleteSnapshot deletes a snapshot.
|
||||
func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
|
||||
|
||||
req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return svc.client.Do(ctx, req, nil)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
// StorageActionsService is an interface for interfacing with the
|
||||
// storage actions endpoints of the Digital Ocean API.
|
||||
// See: https://developers.digitalocean.com/documentation/v2#storage-actions
|
||||
type StorageActionsService interface {
|
||||
Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
|
||||
DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error)
|
||||
Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error)
|
||||
List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error)
|
||||
Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error)
|
||||
}
|
||||
|
||||
// StorageActionsServiceOp handles communication with the storage volumes
|
||||
// action related methods of the DigitalOcean API.
|
||||
type StorageActionsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// StorageAttachment represents the attachement of a block storage
|
||||
// volume to a specific Droplet under the device name.
|
||||
type StorageAttachment struct {
|
||||
DropletID int `json:"droplet_id"`
|
||||
}
|
||||
|
||||
// Attach a storage volume to a Droplet.
|
||||
func (s *StorageActionsServiceOp) Attach(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "attach",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
// DetachByDropletID a storage volume from a Droplet by Droplet ID.
|
||||
func (s *StorageActionsServiceOp) DetachByDropletID(ctx context.Context, volumeID string, dropletID int) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "detach",
|
||||
"droplet_id": dropletID,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
// Get an action for a particular storage volume by id.
|
||||
func (s *StorageActionsServiceOp) Get(ctx context.Context, volumeID string, actionID int) (*Action, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%d", storageAllocationActionPath(volumeID), actionID)
|
||||
return s.get(ctx, path)
|
||||
}
|
||||
|
||||
// List the actions for a particular storage volume.
|
||||
func (s *StorageActionsServiceOp) List(ctx context.Context, volumeID string, opt *ListOptions) ([]Action, *Response, error) {
|
||||
path := storageAllocationActionPath(volumeID)
|
||||
path, err := addOptions(path, opt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return s.list(ctx, path)
|
||||
}
|
||||
|
||||
// Resize a storage volume.
|
||||
func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, sizeGigabytes int, regionSlug string) (*Action, *Response, error) {
|
||||
request := &ActionRequest{
|
||||
"type": "resize",
|
||||
"size_gigabytes": sizeGigabytes,
|
||||
"region": regionSlug,
|
||||
}
|
||||
return s.doAction(ctx, volumeID, request)
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) {
|
||||
path := storageAllocationActionPath(volumeID)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Event, resp, err
|
||||
}
|
||||
|
||||
func (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(actionsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Actions, resp, err
|
||||
}
|
||||
|
||||
func storageAllocationActionPath(volumeID string) string {
|
||||
return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStoragesActions_Attach(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
const (
|
||||
volumeID = "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
|
||||
dropletID = 12345
|
||||
)
|
||||
|
||||
attachRequest := &ActionRequest{
|
||||
"type": "attach",
|
||||
"droplet_id": float64(dropletID), // encoding/json decodes numbers as floats
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(ActionRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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"}]}`)
|
||||
})
|
||||
|
||||
actions, _, err := client.StorageActions.List(ctx, volumeID, nil)
|
||||
if err != nil {
|
||||
t.Errorf("StorageActions.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Action{{Status: "in-progress"}}
|
||||
if !reflect.DeepEqual(actions, expected) {
|
||||
t.Errorf("StorageActions.List returned %+v, expected %+v", actions, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoragesActions_Resize(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
volumeID := "98d414c6-295e-4e3a-ac58-eb9456c1e1d1"
|
||||
|
||||
resizeRequest := &ActionRequest{
|
||||
"type": "resize",
|
||||
"size_gigabytes": float64(500),
|
||||
"region": "nyc1",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/volumes/"+volumeID+"/actions", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(ActionRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStorageVolumes_ListStorageVolumes(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
jBlob := `
|
||||
{
|
||||
"volumes": [
|
||||
{
|
||||
"user_id": 42,
|
||||
"region": {"slug": "nyc3"},
|
||||
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
"name": "my volume",
|
||||
"description": "my description",
|
||||
"size_gigabytes": 100,
|
||||
"droplet_ids": [10],
|
||||
"created_at": "2002-10-02T15:00:00.05Z"
|
||||
},
|
||||
{
|
||||
"user_id": 42,
|
||||
"region": {"slug": "nyc3"},
|
||||
"id": "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
|
||||
"name": "my other volume",
|
||||
"description": "my other description",
|
||||
"size_gigabytes": 100,
|
||||
"created_at": "2012-10-03T15:00:01.05Z"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"pages": {
|
||||
"last": "https://api.digitalocean.com/v2/volumes?page=2",
|
||||
"next": "https://api.digitalocean.com/v2/volumes?page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 28
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/volumes/", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
volumes, _, err := client.Storage.ListVolumes(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Storage.ListVolumes returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Volume{
|
||||
{
|
||||
Region: &Region{Slug: "nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my volume",
|
||||
Description: "my description",
|
||||
SizeGigaBytes: 100,
|
||||
DropletIDs: []int{10},
|
||||
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
|
||||
},
|
||||
{
|
||||
Region: &Region{Slug: "nyc3"},
|
||||
ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
|
||||
Name: "my other volume",
|
||||
Description: "my other description",
|
||||
SizeGigaBytes: 100,
|
||||
CreatedAt: time.Date(2012, 10, 03, 15, 00, 01, 50000000, time.UTC),
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(volumes, expected) {
|
||||
t.Errorf("Storage.ListVolumes returned %+v, expected %+v", volumes, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageVolumes_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
want := &Volume{
|
||||
Region: &Region{Slug: "nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my volume",
|
||||
Description: "my description",
|
||||
SizeGigaBytes: 100,
|
||||
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
|
||||
}
|
||||
jBlob := `{
|
||||
"volume":{
|
||||
"region": {"slug":"nyc3"},
|
||||
"attached_to_droplet": null,
|
||||
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
"name": "my volume",
|
||||
"description": "my description",
|
||||
"size_gigabytes": 100,
|
||||
"created_at": "2002-10-02T15:00:00.05Z"
|
||||
},
|
||||
"links": {
|
||||
"pages": {
|
||||
"last": "https://api.digitalocean.com/v2/volumes?page=2",
|
||||
"next": "https://api.digitalocean.com/v2/volumes?page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 28
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/volumes/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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"
|
||||
}
|
||||
],
|
||||
"links": {},
|
||||
"meta": {
|
||||
"total": 1
|
||||
}
|
||||
}`
|
||||
|
||||
expected := []Volume{
|
||||
{
|
||||
Region: &Region{Slug: "nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "myvolume",
|
||||
Description: "my description",
|
||||
SizeGigaBytes: 100,
|
||||
DropletIDs: []int{10},
|
||||
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("name") != "myvolume" || r.URL.Query().Get("region") != "nyc3" {
|
||||
t.Errorf("Storage.GetVolumeByName did not request the correct name or region")
|
||||
}
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, jBlob)
|
||||
})
|
||||
|
||||
options := &ListVolumeParams{
|
||||
Name: "myvolume",
|
||||
Region: "nyc3",
|
||||
}
|
||||
volumes, _, err := client.Storage.ListVolumes(ctx, options)
|
||||
if err != nil {
|
||||
t.Errorf("Storage.GetVolumeByName returned error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(volumes, expected) {
|
||||
t.Errorf("Storage.GetVolumeByName returned %+v, expected %+v", volumes, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageVolumes_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &VolumeCreateRequest{
|
||||
Region: "nyc3",
|
||||
Name: "my volume",
|
||||
Description: "my description",
|
||||
SizeGigaBytes: 100,
|
||||
}
|
||||
|
||||
want := &Volume{
|
||||
Region: &Region{Slug: "nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my volume",
|
||||
Description: "my description",
|
||||
SizeGigaBytes: 100,
|
||||
CreatedAt: time.Date(2002, 10, 02, 15, 00, 00, 50000000, time.UTC),
|
||||
}
|
||||
jBlob := `{
|
||||
"volume":{
|
||||
"region": {"slug":"nyc3"},
|
||||
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
"name": "my volume",
|
||||
"description": "my description",
|
||||
"size_gigabytes": 100,
|
||||
"created_at": "2002-10-02T15:00:00.05Z"
|
||||
},
|
||||
"links": {}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/volumes", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(VolumeCreateRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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",
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
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"
|
||||
},
|
||||
"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, _, err := client.Storage.ListSnapshots(ctx, "98d414c6-295e-4e3a-ac58-eb9456c1e1d1", nil)
|
||||
if err != nil {
|
||||
t.Errorf("Storage.ListSnapshots returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Snapshot{
|
||||
{
|
||||
Regions: []string{"nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my snapshot",
|
||||
SizeGigaBytes: 100,
|
||||
Created: "2002-10-02T15:00:00.05Z",
|
||||
},
|
||||
{
|
||||
Regions: []string{"nyc3"},
|
||||
ID: "96d414c6-295e-4e3a-ac59-eb9456c1e1d1",
|
||||
Name: "my other snapshot",
|
||||
SizeGigaBytes: 100,
|
||||
Created: "2012-10-03T15:00:01.05Z",
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(volumes, expected) {
|
||||
t.Errorf("Storage.ListSnapshots returned %+v, expected %+v", volumes, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStorageSnapshots_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
want := &Snapshot{
|
||||
Regions: []string{"nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my snapshot",
|
||||
SizeGigaBytes: 100,
|
||||
Created: "2002-10-02T15:00:00.05Z",
|
||||
}
|
||||
jBlob := `{
|
||||
"snapshot":{
|
||||
"regions": ["nyc3"],
|
||||
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
"name": "my snapshot",
|
||||
"size_gigabytes": 100,
|
||||
"created_at": "2002-10-02T15:00:00.05Z"
|
||||
},
|
||||
"links": {
|
||||
"pages": {
|
||||
"last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
|
||||
"next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 28
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/snapshots/80d414c6-295e-4e3a-ac58-eb9456c1e1d1", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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",
|
||||
}
|
||||
|
||||
want := &Snapshot{
|
||||
Regions: []string{"nyc3"},
|
||||
ID: "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
Name: "my snapshot",
|
||||
SizeGigaBytes: 100,
|
||||
Created: "2002-10-02T15:00:00.05Z",
|
||||
}
|
||||
jBlob := `{
|
||||
"snapshot":{
|
||||
"regions": ["nyc3"],
|
||||
"id": "80d414c6-295e-4e3a-ac58-eb9456c1e1d1",
|
||||
"name": "my snapshot",
|
||||
"description": "my description",
|
||||
"size_gigabytes": 100,
|
||||
"created_at": "2002-10-02T15:00:00.05Z"
|
||||
},
|
||||
"links": {
|
||||
"pages": {
|
||||
"last": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2",
|
||||
"next": "https://api.digitalocean.com/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots?page=2"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 28
|
||||
}
|
||||
}`
|
||||
|
||||
mux.HandleFunc("/v2/volumes/98d414c6-295e-4e3a-ac58-eb9456c1e1d1/snapshots", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(SnapshotCreateRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var timestampType = reflect.TypeOf(Timestamp{})
|
||||
|
||||
// Stringify attempts to create a string representation of DigitalOcean types
|
||||
func Stringify(message interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
v := reflect.ValueOf(message)
|
||||
stringifyValue(&buf, v)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// stringifyValue was graciously cargoculted from the goprotubuf library
|
||||
func stringifyValue(w io.Writer, val reflect.Value) {
|
||||
if val.Kind() == reflect.Ptr && val.IsNil() {
|
||||
_, _ = w.Write([]byte("<nil>"))
|
||||
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{'}'})
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
const tagsBasePath = "v2/tags"
|
||||
|
||||
// TagsService is an interface for interfacing with the tags
|
||||
// endpoints of the DigitalOcean API
|
||||
// See: https://developers.digitalocean.com/documentation/v2#tags
|
||||
type TagsService interface {
|
||||
List(context.Context, *ListOptions) ([]Tag, *Response, error)
|
||||
Get(context.Context, string) (*Tag, *Response, error)
|
||||
Create(context.Context, *TagCreateRequest) (*Tag, *Response, error)
|
||||
Delete(context.Context, string) (*Response, error)
|
||||
|
||||
TagResources(context.Context, string, *TagResourcesRequest) (*Response, error)
|
||||
UntagResources(context.Context, string, *UntagResourcesRequest) (*Response, error)
|
||||
}
|
||||
|
||||
// TagsServiceOp handles communication with tag related method of the
|
||||
// DigitalOcean API.
|
||||
type TagsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ TagsService = &TagsServiceOp{}
|
||||
|
||||
// ResourceType represents a class of resource, currently only droplet are supported
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
//DropletResourceType holds the string representing our ResourceType of Droplet.
|
||||
DropletResourceType ResourceType = "droplet"
|
||||
)
|
||||
|
||||
// Resource represent a single resource for associating/disassociating with tags
|
||||
type Resource struct {
|
||||
ID string `json:"resource_id,omit_empty"`
|
||||
Type ResourceType `json:"resource_type,omit_empty"`
|
||||
}
|
||||
|
||||
// TaggedResources represent the set of resources a tag is attached to
|
||||
type TaggedResources struct {
|
||||
Droplets *TaggedDropletsResources `json:"droplets,omitempty"`
|
||||
}
|
||||
|
||||
// TaggedDropletsResources represent the droplet resources a tag is attached to
|
||||
type TaggedDropletsResources struct {
|
||||
Count int `json:"count,float64,omitempty"`
|
||||
LastTagged *Droplet `json:"last_tagged,omitempty"`
|
||||
}
|
||||
|
||||
// Tag represent DigitalOcean tag
|
||||
type Tag struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Resources *TaggedResources `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
//TagCreateRequest represents the JSON structure of a request of that type.
|
||||
type TagCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TagResourcesRequest represents the JSON structure of a request of that type.
|
||||
type TagResourcesRequest struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
// UntagResourcesRequest represents the JSON structure of a request of that type.
|
||||
type UntagResourcesRequest struct {
|
||||
Resources []Resource `json:"resources"`
|
||||
}
|
||||
|
||||
type tagsRoot struct {
|
||||
Tags []Tag `json:"tags"`
|
||||
Links *Links `json:"links"`
|
||||
}
|
||||
|
||||
type tagRoot struct {
|
||||
Tag *Tag `json:"tag"`
|
||||
}
|
||||
|
||||
// List all tags
|
||||
func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Response, error) {
|
||||
path := tagsBasePath
|
||||
path, err := addOptions(path, opt)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagsRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
if l := root.Links; l != nil {
|
||||
resp.Links = l
|
||||
}
|
||||
|
||||
return root.Tags, resp, err
|
||||
}
|
||||
|
||||
// Get a single tag
|
||||
func (s *TagsServiceOp) Get(ctx context.Context, name string) (*Tag, *Response, error) {
|
||||
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Tag, resp, err
|
||||
}
|
||||
|
||||
// Create a new tag
|
||||
func (s *TagsServiceOp) Create(ctx context.Context, createRequest *TagCreateRequest) (*Tag, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, NewArgError("createRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, tagsBasePath, createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(tagRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Tag, resp, err
|
||||
}
|
||||
|
||||
// Delete an existing tag
|
||||
func (s *TagsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// TagResources associates resources with a given Tag.
|
||||
func (s *TagsServiceOp) TagResources(ctx context.Context, name string, tagRequest *TagResourcesRequest) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
if tagRequest == nil {
|
||||
return nil, NewArgError("tagRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodPost, path, tagRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// UntagResources dissociates resources with a given Tag.
|
||||
func (s *TagsServiceOp) UntagResources(ctx context.Context, name string, untagRequest *UntagResourcesRequest) (*Response, error) {
|
||||
if name == "" {
|
||||
return nil, NewArgError("name", "cannot be empty")
|
||||
}
|
||||
|
||||
if untagRequest == nil {
|
||||
return nil, NewArgError("tagRequest", "cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
|
||||
req, err := s.client.NewRequest(ctx, http.MethodDelete, path, untagRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
|
||||
return resp, err
|
||||
}
|
|
@ -0,0 +1,355 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
listEmptyJSON = `
|
||||
{
|
||||
"tags": [
|
||||
],
|
||||
"meta": {
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
listJSON = `
|
||||
{
|
||||
"tags": [
|
||||
{
|
||||
"name": "testing-1",
|
||||
"resources": {
|
||||
"droplets": {
|
||||
"count": 0,
|
||||
"last_tagged": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "testing-2",
|
||||
"resources": {
|
||||
"droplets": {
|
||||
"count": 0,
|
||||
"last_tagged": null
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"pages":{
|
||||
"next":"http://example.com/v2/tags/?page=3",
|
||||
"prev":"http://example.com/v2/tags/?page=1",
|
||||
"last":"http://example.com/v2/tags/?page=3",
|
||||
"first":"http://example.com/v2/tags/?page=1"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
createJSON = `
|
||||
{
|
||||
"tag": {
|
||||
"name": "testing-1",
|
||||
"resources": {
|
||||
"droplets": {
|
||||
"count": 0,
|
||||
"last_tagged": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
getJSON = `
|
||||
{
|
||||
"tag": {
|
||||
"name": "testing-1",
|
||||
"resources": {
|
||||
"droplets": {
|
||||
"count": 1,
|
||||
"last_tagged": {
|
||||
"id": 1,
|
||||
"name": "test.example.com",
|
||||
"memory": 1024,
|
||||
"vcpus": 2,
|
||||
"disk": 20,
|
||||
"region": {
|
||||
"slug": "nyc1",
|
||||
"name": "New York",
|
||||
"sizes": [
|
||||
"1024mb",
|
||||
"512mb"
|
||||
],
|
||||
"available": true,
|
||||
"features": [
|
||||
"virtio",
|
||||
"private_networking",
|
||||
"backups",
|
||||
"ipv6"
|
||||
]
|
||||
},
|
||||
"image": {
|
||||
"id": 119192817,
|
||||
"name": "Ubuntu 13.04",
|
||||
"distribution": "ubuntu",
|
||||
"slug": "ubuntu1304",
|
||||
"public": true,
|
||||
"regions": [
|
||||
"nyc1"
|
||||
],
|
||||
"created_at": "2014-07-29T14:35:37Z"
|
||||
},
|
||||
"size_slug": "1024mb",
|
||||
"locked": false,
|
||||
"status": "active",
|
||||
"networks": {
|
||||
"v4": [
|
||||
{
|
||||
"ip_address": "10.0.0.19",
|
||||
"netmask": "255.255.0.0",
|
||||
"gateway": "10.0.0.1",
|
||||
"type": "private"
|
||||
},
|
||||
{
|
||||
"ip_address": "127.0.0.19",
|
||||
"netmask": "255.255.255.0",
|
||||
"gateway": "127.0.0.20",
|
||||
"type": "public"
|
||||
}
|
||||
],
|
||||
"v6": [
|
||||
{
|
||||
"ip_address": "2001::13",
|
||||
"cidr": 124,
|
||||
"gateway": "2400:6180:0000:00D0:0000:0000:0009:7000",
|
||||
"type": "public"
|
||||
}
|
||||
]
|
||||
},
|
||||
"kernel": {
|
||||
"id": 485432985,
|
||||
"name": "DO-recovery-static-fsck",
|
||||
"version": "3.8.0-25-generic"
|
||||
},
|
||||
"created_at": "2014-07-29T14:35:37Z",
|
||||
"features": [
|
||||
"ipv6"
|
||||
],
|
||||
"backup_ids": [
|
||||
449676382
|
||||
],
|
||||
"snapshot_ids": [
|
||||
449676383
|
||||
],
|
||||
"action_ids": [
|
||||
],
|
||||
"tags": [
|
||||
"tag-1",
|
||||
"tag-2"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func TestTags_List(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, listJSON)
|
||||
})
|
||||
|
||||
tags, _, err := client.Tags.List(ctx, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Tags.List returned error: %v", err)
|
||||
}
|
||||
|
||||
expected := []Tag{{Name: "testing-1", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}},
|
||||
{Name: "testing-2", Resources: &TaggedResources{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}}
|
||||
if !reflect.DeepEqual(tags, expected) {
|
||||
t.Errorf("Tags.List returned %+v, expected %+v", tags, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTags_ListEmpty(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, 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.Droplets.Count != 1 {
|
||||
t.Errorf("Tags.Get return an incorrect droplet resource count, got %+v, expected %+v", tag.Resources.Droplets.Count, 1)
|
||||
}
|
||||
|
||||
if tag.Resources.Droplets.LastTagged.ID != 1 {
|
||||
t.Errorf("Tags.Get return an incorrect last tagged droplet %+v, expected %+v", tag.Resources.Droplets.LastTagged.ID, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTags_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
createRequest := &TagCreateRequest{
|
||||
Name: "testing-1",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/v2/tags", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := new(TagCreateRequest)
|
||||
err := json.NewDecoder(r.Body).Decode(v)
|
||||
if err != nil {
|
||||
t.Fatalf("decode json: %v", err)
|
||||
}
|
||||
|
||||
testMethod(t, r, 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{Droplets: &TaggedDropletsResources{Count: 0, LastTagged: nil}}}
|
||||
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}},
|
||||
}
|
||||
|
||||
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}},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package godo
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timestamp represents a time that can be unmarshalled from a JSON string
|
||||
// formatted as either an RFC3339 or Unix timestamp. All
|
||||
// exported methods of time.Time can be called on Timestamp.
|
||||
type Timestamp struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (t Timestamp) String() string {
|
||||
return t.Time.String()
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// Time is expected in RFC3339 or Unix format.
|
||||
func (t *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
str := string(data)
|
||||
i, err := strconv.ParseInt(str, 10, 64)
|
||||
if err == nil {
|
||||
t.Time = time.Unix(i, 0)
|
||||
} else {
|
||||
t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Equal reports whether t and u are equal based on time.Equal
|
||||
func (t Timestamp) Equal(u Timestamp) bool {
|
||||
return t.Time.Equal(u.Time)
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
package 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/digitalocean/godo/context"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue