digitalocean tag support (#7500)

* vendor: update godo to support tags

* digitalocean: introduce tag resource

* website: update for digitalocean_tag resource
This commit is contained in:
Tommy Murphy 2016-07-11 07:09:06 -04:00 committed by Paul Stack
parent d70b9d334b
commit 0c6856f85c
17 changed files with 1253 additions and 33 deletions

View File

@ -23,6 +23,7 @@ func Provider() terraform.ResourceProvider {
"digitalocean_floating_ip": resourceDigitalOceanFloatingIp(),
"digitalocean_record": resourceDigitalOceanRecord(),
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),
"digitalocean_tag": resourceDigitalOceanTag(),
},
ConfigureFunc: providerConfigure,

View File

@ -104,6 +104,12 @@ func resourceDigitalOceanDroplet() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
},
"tags": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"user_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -181,6 +187,12 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{})
"Error waiting for droplet (%s) to become ready: %s", d.Id(), err)
}
// droplet needs to be active in order to set tags
err = setTags(client, d)
if err != nil {
return fmt.Errorf("Error setting tags: %s", err)
}
return resourceDigitalOceanDropletRead(d, meta)
}
@ -236,6 +248,8 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e
"host": findIPv4AddrByType(droplet, "public"),
})
d.Set("tags", droplet.Tags)
return nil
}
@ -379,6 +393,13 @@ func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{})
}
}
if d.HasChange("tags") {
err = setTags(client, d)
if err != nil {
return fmt.Errorf("Error updating tags: %s", err)
}
}
return resourceDigitalOceanDropletRead(d, meta)
}

View File

@ -103,6 +103,40 @@ func TestAccDigitalOceanDroplet_UpdateUserData(t *testing.T) {
})
}
func TestAccDigitalOceanDroplet_UpdateTags(t *testing.T) {
var afterCreate, afterUpdate godo.Droplet
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanDropletDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDigitalOceanDropletConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &afterCreate),
testAccCheckDigitalOceanDropletAttributes(&afterCreate),
),
},
resource.TestStep{
Config: testAccCheckDigitalOceanDropletConfig_tag_update,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &afterUpdate),
resource.TestCheckResourceAttr(
"digitalocean_droplet.foobar",
"tags.#",
"1"),
resource.TestCheckResourceAttr(
"digitalocean_droplet.foobar",
"tags.0",
"barbaz"),
),
},
},
})
}
func TestAccDigitalOceanDroplet_PrivateNetworkingIpv6(t *testing.T) {
var droplet godo.Droplet
@ -309,6 +343,27 @@ resource "digitalocean_droplet" "foobar" {
}
`, testAccValidPublicKey)
var testAccCheckDigitalOceanDropletConfig_tag_update = fmt.Sprintf(`
resource "digitalocean_tag" "barbaz" {
name = "barbaz"
}
resource "digitalocean_ssh_key" "foobar" {
name = "foobar"
public_key = "%s"
}
resource "digitalocean_droplet" "foobar" {
name = "foo"
size = "512mb"
image = "centos-5-8-x32"
region = "nyc3"
user_data = "foobar"
ssh_keys = ["${digitalocean_ssh_key.foobar.id}"]
tags = ["${digitalocean_tag.barbaz.id}"]
}
`, testAccValidPublicKey)
var testAccCheckDigitalOceanDropletConfig_userdata_update = fmt.Sprintf(`
resource "digitalocean_ssh_key" "foobar" {
name = "foobar"

View File

@ -0,0 +1,104 @@
package digitalocean
import (
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceDigitalOceanTag() *schema.Resource {
return &schema.Resource{
Create: resourceDigitalOceanTagCreate,
Read: resourceDigitalOceanTagRead,
Update: resourceDigitalOceanTagUpdate,
Delete: resourceDigitalOceanTagDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
}
}
func resourceDigitalOceanTagCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
// Build up our creation options
opts := &godo.TagCreateRequest{
Name: d.Get("name").(string),
}
log.Printf("[DEBUG] Tag create configuration: %#v", opts)
tag, _, err := client.Tags.Create(opts)
if err != nil {
return fmt.Errorf("Error creating tag: %s", err)
}
d.SetId(tag.Name)
log.Printf("[INFO] Tag: %s", tag.Name)
return resourceDigitalOceanTagRead(d, meta)
}
func resourceDigitalOceanTagRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
tag, resp, err := client.Tags.Get(d.Id())
if err != nil {
// If the tag is somehow already destroyed, mark as
// successfully gone
if resp != nil && resp.StatusCode == 404 {
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving tag: %s", err)
}
d.Set("name", tag.Name)
return nil
}
func resourceDigitalOceanTagUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
var newName string
if v, ok := d.GetOk("name"); ok {
newName = v.(string)
}
log.Printf("[DEBUG] tag update name: %#v", newName)
opts := &godo.TagUpdateRequest{
Name: newName,
}
_, err := client.Tags.Update(d.Id(), opts)
if err != nil {
return fmt.Errorf("Failed to update tag: %s", err)
}
d.Set("name", newName)
return resourceDigitalOceanTagRead(d, meta)
}
func resourceDigitalOceanTagDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
log.Printf("[INFO] Deleting tag: %s", d.Id())
_, err := client.Tags.Delete(d.Id())
if err != nil {
return fmt.Errorf("Error deleting tag: %s", err)
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,93 @@
package digitalocean
import (
"fmt"
"testing"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccDigitalOceanTag_Basic(t *testing.T) {
var tag godo.Tag
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanTagDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccCheckDigitalOceanTagConfig_basic,
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanTagExists("digitalocean_tag.foobar", &tag),
testAccCheckDigitalOceanTagAttributes(&tag),
resource.TestCheckResourceAttr(
"digitalocean_tag.foobar", "name", "foobar"),
),
},
},
})
}
func testAccCheckDigitalOceanTagDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_tag" {
continue
}
// Try to find the key
_, _, err := client.Tags.Get(rs.Primary.ID)
if err == nil {
return fmt.Errorf("Tag still exists")
}
}
return nil
}
func testAccCheckDigitalOceanTagAttributes(tag *godo.Tag) resource.TestCheckFunc {
return func(s *terraform.State) error {
if tag.Name != "foobar" {
return fmt.Errorf("Bad name: %s", tag.Name)
}
return nil
}
}
func testAccCheckDigitalOceanTagExists(n string, tag *godo.Tag) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Record ID is set")
}
client := testAccProvider.Meta().(*godo.Client)
// Try to find the tag
foundTag, _, err := client.Tags.Get(rs.Primary.ID)
if err != nil {
return err
}
*tag = *foundTag
return nil
}
}
var testAccCheckDigitalOceanTagConfig_basic = fmt.Sprintf(`
resource "digitalocean_tag" "foobar" {
name = "foobar"
}`)

View File

@ -0,0 +1,72 @@
package digitalocean
import (
"log"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
)
// setTags is a helper to set the tags for a resource. It expects the
// tags field to be named "tags"
func setTags(conn *godo.Client, d *schema.ResourceData) error {
oraw, nraw := d.GetChange("tags")
remove, create := diffTags(tagsFromSchema(oraw), tagsFromSchema(nraw))
log.Printf("[DEBUG] Removing tags: %#v from %s", remove, d.Id())
for _, tag := range remove {
_, err := conn.Tags.UntagResources(tag, &godo.UntagResourcesRequest{
Resources: []godo.Resource{
godo.Resource{
ID: d.Id(),
Type: godo.DropletResourceType,
},
},
})
if err != nil {
return err
}
}
log.Printf("[DEBUG] Creating tags: %s for %s", create, d.Id())
for _, tag := range create {
_, err := conn.Tags.TagResources(tag, &godo.TagResourcesRequest{
Resources: []godo.Resource{
godo.Resource{
ID: d.Id(),
Type: godo.DropletResourceType,
},
},
})
if err != nil {
return err
}
}
return nil
}
// tagsFromSchema takes the raw schema tags and returns them as a
// properly asserted map[string]string
func tagsFromSchema(raw interface{}) map[string]string {
result := make(map[string]string)
for _, t := range raw.([]interface{}) {
result[t.(string)] = t.(string)
}
return result
}
// diffTags takes the old and the new tag sets and returns the difference of
// both. The remaining tags are those that need to be removed and created
func diffTags(oldTags, newTags map[string]string) (map[string]string, map[string]string) {
for k := range oldTags {
_, ok := newTags[k]
if ok {
delete(newTags, k)
delete(oldTags, k)
}
}
return oldTags, newTags
}

View File

@ -0,0 +1,51 @@
package digitalocean
import (
"reflect"
"testing"
)
func TestDiffTags(t *testing.T) {
cases := []struct {
Old, New []interface{}
Create, Remove map[string]string
}{
// Basic add/remove
{
Old: []interface{}{
"foo",
},
New: []interface{}{
"bar",
},
Create: map[string]string{
"bar": "bar",
},
Remove: map[string]string{
"foo": "foo",
},
},
// Noop
{
Old: []interface{}{
"foo",
},
New: []interface{}{
"foo",
},
Create: map[string]string{},
Remove: map[string]string{},
},
}
for i, tc := range cases {
r, c := diffTags(tagsFromSchema(tc.Old), tagsFromSchema(tc.New))
if !reflect.DeepEqual(r, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, r)
}
if !reflect.DeepEqual(c, tc.Create) {
t.Fatalf("%d: bad create: %#v", i, c)
}
}
}

View File

@ -1,6 +0,0 @@
language: go
go:
- 1.3
- 1.4
- tip

View File

@ -13,22 +13,31 @@ type ActionRequest map[string]interface{}
// See: https://developers.digitalocean.com/documentation/v2#droplet-actions
type DropletActionsService interface {
Shutdown(int) (*Action, *Response, error)
ShutdownByTag(string) (*Action, *Response, error)
PowerOff(int) (*Action, *Response, error)
PowerOffByTag(string) (*Action, *Response, error)
PowerOn(int) (*Action, *Response, error)
PowerOnByTag(string) (*Action, *Response, error)
PowerCycle(int) (*Action, *Response, error)
PowerCycleByTag(string) (*Action, *Response, error)
Reboot(int) (*Action, *Response, error)
Restore(int, int) (*Action, *Response, error)
Resize(int, string, bool) (*Action, *Response, error)
Rename(int, string) (*Action, *Response, error)
Snapshot(int, string) (*Action, *Response, error)
SnapshotByTag(string, string) (*Action, *Response, error)
EnableBackups(int) (*Action, *Response, error)
EnableBackupsByTag(string) (*Action, *Response, error)
DisableBackups(int) (*Action, *Response, error)
DisableBackupsByTag(string) (*Action, *Response, error)
PasswordReset(int) (*Action, *Response, error)
RebuildByImageID(int, int) (*Action, *Response, error)
RebuildByImageSlug(int, string) (*Action, *Response, error)
ChangeKernel(int, int) (*Action, *Response, error)
EnableIPv6(int) (*Action, *Response, error)
EnableIPv6ByTag(string) (*Action, *Response, error)
EnablePrivateNetworking(int) (*Action, *Response, error)
EnablePrivateNetworkingByTag(string) (*Action, *Response, error)
Upgrade(int) (*Action, *Response, error)
Get(int, int) (*Action, *Response, error)
GetByURI(string) (*Action, *Response, error)
@ -48,24 +57,48 @@ func (s *DropletActionsServiceOp) Shutdown(id int) (*Action, *Response, error) {
return s.doAction(id, request)
}
// Shutdown Droplets by Tag
func (s *DropletActionsServiceOp) ShutdownByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "shutdown"}
return s.doActionByTag(tag, request)
}
// PowerOff a Droplet
func (s *DropletActionsServiceOp) PowerOff(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_off"}
return s.doAction(id, request)
}
// PowerOff a Droplet by Tag
func (s *DropletActionsServiceOp) PowerOffByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_off"}
return s.doActionByTag(tag, request)
}
// PowerOn a Droplet
func (s *DropletActionsServiceOp) PowerOn(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_on"}
return s.doAction(id, request)
}
// PowerOn a Droplet by Tag
func (s *DropletActionsServiceOp) PowerOnByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_on"}
return s.doActionByTag(tag, request)
}
// PowerCycle a Droplet
func (s *DropletActionsServiceOp) PowerCycle(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_cycle"}
return s.doAction(id, request)
}
// PowerCycle a Droplet by Tag
func (s *DropletActionsServiceOp) PowerCycleByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "power_cycle"}
return s.doActionByTag(tag, request)
}
// Reboot a Droplet
func (s *DropletActionsServiceOp) Reboot(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "reboot"}
@ -113,18 +146,40 @@ func (s *DropletActionsServiceOp) Snapshot(id int, name string) (*Action, *Respo
return s.doAction(id, request)
}
// Snapshot a Droplet by Tag
func (s *DropletActionsServiceOp) SnapshotByTag(tag string, name string) (*Action, *Response, error) {
requestType := "snapshot"
request := &ActionRequest{
"type": requestType,
"name": name,
}
return s.doActionByTag(tag, request)
}
// EnableBackups enables backups for a droplet.
func (s *DropletActionsServiceOp) EnableBackups(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_backups"}
return s.doAction(id, request)
}
// EnableBackups enables backups for a droplet by Tag
func (s *DropletActionsServiceOp) EnableBackupsByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_backups"}
return s.doActionByTag(tag, request)
}
// DisableBackups disables backups for a droplet.
func (s *DropletActionsServiceOp) DisableBackups(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "disable_backups"}
return s.doAction(id, request)
}
// DisableBackups disables backups for a droplet by tag
func (s *DropletActionsServiceOp) DisableBackupsByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "disable_backups"}
return s.doActionByTag(tag, request)
}
// PasswordReset resets the password for a droplet.
func (s *DropletActionsServiceOp) PasswordReset(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "password_reset"}
@ -155,12 +210,24 @@ func (s *DropletActionsServiceOp) EnableIPv6(id int) (*Action, *Response, error)
return s.doAction(id, request)
}
// EnableIPv6 enables IPv6 for a droplet by Tag
func (s *DropletActionsServiceOp) EnableIPv6ByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_ipv6"}
return s.doActionByTag(tag, request)
}
// EnablePrivateNetworking enables private networking for a droplet.
func (s *DropletActionsServiceOp) EnablePrivateNetworking(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_private_networking"}
return s.doAction(id, request)
}
// EnablePrivateNetworking enables private networking for a droplet by Tag
func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(tag string) (*Action, *Response, error) {
request := &ActionRequest{"type": "enable_private_networking"}
return s.doActionByTag(tag, request)
}
// Upgrade a droplet.
func (s *DropletActionsServiceOp) Upgrade(id int) (*Action, *Response, error) {
request := &ActionRequest{"type": "upgrade"}
@ -192,6 +259,31 @@ func (s *DropletActionsServiceOp) doAction(id int, request *ActionRequest) (*Act
return &root.Event, resp, err
}
func (s *DropletActionsServiceOp) doActionByTag(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("POST", path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
// Get an action for a particular droplet by id.
func (s *DropletActionsServiceOp) Get(dropletID, actionID int) (*Action, *Response, error) {
if dropletID < 1 {
@ -236,3 +328,7 @@ func (s *DropletActionsServiceOp) get(path string) (*Action, *Response, error) {
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)
}

View File

@ -2,20 +2,25 @@ package godo
import (
"encoding/json"
"errors"
"fmt"
)
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(*ListOptions) ([]Droplet, *Response, error)
ListByTag(string, *ListOptions) ([]Droplet, *Response, error)
Get(int) (*Droplet, *Response, error)
Create(*DropletCreateRequest) (*Droplet, *Response, error)
CreateMultiple(*DropletMultiCreateRequest) ([]Droplet, *Response, error)
Delete(int) (*Response, error)
DeleteByTag(string) (*Response, error)
Kernels(int, *ListOptions) ([]Kernel, *Response, error)
Snapshots(int, *ListOptions) ([]Image, *Response, error)
Backups(int, *ListOptions) ([]Image, *Response, error)
@ -47,9 +52,55 @@ type Droplet struct {
Locked bool `json:"locked,bool,omitempty"`
Status string `json:"status,omitempty"`
Networks *Networks `json:"networks,omitempty"`
ActionIDs []int `json:"action_ids,omitempty"`
Created string `json:"created_at,omitempty"`
Kernel *Kernel `json:"kernel,omitempty"`
Tags []string `json:"tags,ommitempty"`
VolumeIDs []string `json:"volumes"`
}
// 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 private IPv6 address for the Droplet.
func (d *Droplet) PublicIPv6() (string, error) {
if d.Networks == nil {
return "", errNoNetworks
}
for _, v4 := range d.Networks.V6 {
if v4.Type == "public" {
return v4.IPAddress, nil
}
}
return "", nil
}
// Kernel object
@ -96,6 +147,27 @@ type DropletCreateImage struct {
Slug string
}
// 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})
}
// 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) {
@ -133,9 +205,10 @@ type DropletCreateRequest struct {
IPv6 bool `json:"ipv6"`
PrivateNetworking bool `json:"private_networking"`
UserData string `json:"user_data,omitempty"`
Volumes []DropletCreateVolume `json:"volumes,omitempty"`
}
// DropletMultiCreateRequest is a request to create multiple droplets.
type DropletMultiCreateRequest struct {
Names []string `json:"names"`
Region string `json:"region"`
@ -186,14 +259,8 @@ func (n NetworkV6) String() string {
return Stringify(n)
}
// List all droplets
func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) {
path := dropletBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
// Performs a list request given a path
func (s *DropletsServiceOp) list(path string) ([]Droplet, *Response, error) {
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
@ -211,6 +278,28 @@ func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error)
return root.Droplets, resp, err
}
// List all droplets
func (s *DropletsServiceOp) List(opt *ListOptions) ([]Droplet, *Response, error) {
path := dropletBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
return s.list(path)
}
// List all droplets by tag
func (s *DropletsServiceOp) ListByTag(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(path)
}
// Get individual droplet
func (s *DropletsServiceOp) Get(dropletID int) (*Droplet, *Response, error) {
if dropletID < 1 {
@ -258,7 +347,7 @@ func (s *DropletsServiceOp) Create(createRequest *DropletCreateRequest) (*Drople
return root.Droplet, resp, err
}
// Create multiple droplet
// CreateMultiple creates multiple droplets.
func (s *DropletsServiceOp) CreateMultiple(createRequest *DropletMultiCreateRequest) ([]Droplet, *Response, error) {
if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil")
@ -283,14 +372,8 @@ func (s *DropletsServiceOp) CreateMultiple(createRequest *DropletMultiCreateRequ
return root.Droplets, resp, err
}
// Delete droplet
func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) {
if dropletID < 1 {
return nil, NewArgError("dropletID", "cannot be less than 1")
}
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
// Performs a delete request given a path
func (s *DropletsServiceOp) delete(path string) (*Response, error) {
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
@ -301,6 +384,28 @@ func (s *DropletsServiceOp) Delete(dropletID int) (*Response, error) {
return resp, err
}
// Delete droplet
func (s *DropletsServiceOp) Delete(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(path)
}
// Delete droplets by tag
func (s *DropletsServiceOp) DeleteByTag(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(path)
}
// Kernels lists kernels available for a droplet.
func (s *DropletsServiceOp) Kernels(dropletID int, opt *ListOptions) ([]Kernel, *Response, error) {
if dropletID < 1 {

View File

@ -22,9 +22,9 @@ const (
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
headerRateLimit = "X-RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining"
headerRateReset = "X-RateLimit-Reset"
headerRateLimit = "RateLimit-Limit"
headerRateRemaining = "RateLimit-Remaining"
headerRateReset = "RateLimit-Reset"
)
// Client manages communication with DigitalOcean V2 API.
@ -55,6 +55,9 @@ type Client struct {
Sizes SizesService
FloatingIPs FloatingIPsService
FloatingIPActions FloatingIPActionsService
Storage StorageService
StorageActions StorageActionsService
Tags TagsService
// Optional function called after every successful request made to the DO APIs
onRequestCompleted RequestCompletionCallback
@ -93,7 +96,10 @@ type ErrorResponse struct {
Response *http.Response
// Error message
Message string
Message string `json:"message"`
// RequestID returned from the API, useful to contact support.
RequestID string `json:"request_id"`
}
// Rate contains the rate limit for the current client.
@ -156,10 +162,49 @@ func NewClient(httpClient *http.Client) *Client {
c.Sizes = &SizesServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.Storage = &StorageServiceOp{client: c}
c.StorageActions = &StorageActionsServiceOp{client: c}
c.Tags = &TagsServiceOp{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.
@ -186,7 +231,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
req.Header.Add("Content-Type", mediaType)
req.Header.Add("Accept", mediaType)
req.Header.Add("User-Agent", userAgent)
req.Header.Add("User-Agent", c.UserAgent)
return req, nil
}
@ -280,6 +325,10 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
return response, err
}
func (r *ErrorResponse) Error() string {
if r.RequestID != "" {
return fmt.Sprintf("%v %v: %d (request %q) %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.RequestID, r.Message)
}
return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message)
}

252
vendor/github.com/digitalocean/godo/storage.go generated vendored Normal file
View File

@ -0,0 +1,252 @@
package godo
import (
"fmt"
"time"
)
const (
storageBasePath = "v2"
storageAllocPath = storageBasePath + "/volumes"
storageSnapPath = storageBasePath + "/snapshots"
)
// StorageService is an interface for interfacing with the storage
// endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#storage
type StorageService interface {
ListVolumes(*ListOptions) ([]Volume, *Response, error)
GetVolume(string) (*Volume, *Response, error)
CreateVolume(*VolumeCreateRequest) (*Volume, *Response, error)
DeleteVolume(string) (*Response, error)
}
// BetaStorageService is an interface for the storage services that are
// not yet stable. The interface is not exposed in the godo.Client and
// requires type-asserting the `StorageService` to make it available.
//
// Note that Beta features will change and compiling against those
// symbols (using type-assertion) is prone to breaking your build
// if you use our master.
type BetaStorageService interface {
StorageService
ListSnapshots(volumeID string, opts *ListOptions) ([]Snapshot, *Response, error)
GetSnapshot(string) (*Snapshot, *Response, error)
CreateSnapshot(*SnapshotCreateRequest) (*Snapshot, *Response, error)
DeleteSnapshot(string) (*Response, error)
}
// StorageServiceOp handles communication with the storage volumes related methods of the
// DigitalOcean API.
type StorageServiceOp struct {
client *Client
}
var _ StorageService = &StorageServiceOp{}
// Volume represents a Digital Ocean block store volume.
type Volume struct {
ID string `json:"id"`
Region *Region `json:"region"`
Name string `json:"name"`
SizeGigaBytes int64 `json:"size_gigabytes"`
Description string `json:"description"`
DropletIDs []int `json:"droplet_ids"`
CreatedAt time.Time `json:"created_at"`
}
func (f Volume) String() string {
return Stringify(f)
}
type storageVolumesRoot struct {
Volumes []Volume `json:"volumes"`
Links *Links `json:"links"`
}
type storageVolumeRoot struct {
Volume *Volume `json:"volume"`
Links *Links `json:"links,omitempty"`
}
// VolumeCreateRequest represents a request to create a block store
// volume.
type VolumeCreateRequest struct {
Region string `json:"region"`
Name string `json:"name"`
Description string `json:"description"`
SizeGigaBytes int64 `json:"size_gigabytes"`
}
// ListVolumes lists all storage volumes.
func (svc *StorageServiceOp) ListVolumes(opt *ListOptions) ([]Volume, *Response, error) {
path, err := addOptions(storageAllocPath, opt)
if err != nil {
return nil, nil, err
}
req, err := svc.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumesRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Volumes, resp, nil
}
// CreateVolume creates a storage volume. The name must be unique.
func (svc *StorageServiceOp) CreateVolume(createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
path := storageAllocPath
req, err := svc.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Volume, resp, nil
}
// GetVolume retrieves an individual storage volume.
func (svc *StorageServiceOp) GetVolume(id string) (*Volume, *Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
req, err := svc.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Volume, resp, nil
}
// DeleteVolume deletes a storage volume.
func (svc *StorageServiceOp) DeleteVolume(id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
req, err := svc.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
return svc.client.Do(req, nil)
}
// Snapshot represents a Digital Ocean block store snapshot.
type Snapshot struct {
ID string `json:"id"`
VolumeID string `json:"volume_id"`
Region *Region `json:"region"`
Name string `json:"name"`
SizeGigaBytes int64 `json:"size_gigabytes"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
}
type storageSnapsRoot struct {
Snapshots []Snapshot `json:"snapshots"`
Links *Links `json:"links"`
}
type storageSnapRoot struct {
Snapshot *Snapshot `json:"snapshot"`
Links *Links `json:"links,omitempty"`
}
// 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(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("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageSnapsRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Snapshots, resp, nil
}
// CreateSnapshot creates a snapshot of a storage volume.
func (svc *StorageServiceOp) CreateSnapshot(createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
req, err := svc.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(storageSnapRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Snapshot, resp, nil
}
// GetSnapshot retrieves an individual snapshot.
func (svc *StorageServiceOp) GetSnapshot(id string) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
req, err := svc.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageSnapRoot)
resp, err := svc.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Snapshot, resp, nil
}
// DeleteSnapshot deletes a snapshot.
func (svc *StorageServiceOp) DeleteSnapshot(id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
req, err := svc.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
return svc.client.Do(req, nil)
}

61
vendor/github.com/digitalocean/godo/storage_actions.go generated vendored Normal file
View File

@ -0,0 +1,61 @@
package godo
import "fmt"
// StorageActionsService is an interface for interfacing with the
// storage actions endpoints of the Digital Ocean API.
// See: https://developers.digitalocean.com/documentation/v2#storage-actions
type StorageActionsService interface {
Attach(volumeID string, dropletID int) (*Action, *Response, error)
Detach(volumeID string) (*Action, *Response, error)
}
// StorageActionsServiceOp handles communication with the floating IPs
// 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(volumeID string, dropletID int) (*Action, *Response, error) {
request := &ActionRequest{
"type": "attach",
"droplet_id": dropletID,
}
return s.doAction(volumeID, request)
}
// Detach a storage volume from a droplet.
func (s *StorageActionsServiceOp) Detach(volumeID string) (*Action, *Response, error) {
request := &ActionRequest{
"type": "detach",
}
return s.doAction(volumeID, request)
}
func (s *StorageActionsServiceOp) doAction(volumeID string, request *ActionRequest) (*Action, *Response, error) {
path := storageAllocationActionPath(volumeID)
req, err := s.client.NewRequest("POST", path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return &root.Event, resp, err
}
func storageAllocationActionPath(volumeID string) string {
return fmt.Sprintf("%s/%s/actions", storageAllocPath, volumeID)
}

226
vendor/github.com/digitalocean/godo/tags.go generated vendored Normal file
View File

@ -0,0 +1,226 @@
package godo
import "fmt"
const tagsBasePath = "v2/tags"
// TagsService is an interface for interfacing with the tags
// endpoints of the DigitalOcean API
// See: https://developers.digitalocean.com/documentation/v2#tags
type TagsService interface {
List(*ListOptions) ([]Tag, *Response, error)
Get(string) (*Tag, *Response, error)
Create(*TagCreateRequest) (*Tag, *Response, error)
Update(string, *TagUpdateRequest) (*Response, error)
Delete(string) (*Response, error)
TagResources(string, *TagResourcesRequest) (*Response, error)
UntagResources(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 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"`
}
type TagCreateRequest struct {
Name string `json:"name"`
}
type TagUpdateRequest struct {
Name string `json:"name"`
}
type TagResourcesRequest struct {
Resources []Resource `json:"resources"`
}
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(opt *ListOptions) ([]Tag, *Response, error) {
path := tagsBasePath
path, err := addOptions(path, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
if l := root.Links; l != nil {
resp.Links = l
}
return root.Tags, resp, err
}
// Get a single tag
func (s *TagsServiceOp) Get(name string) (*Tag, *Response, error) {
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Tag, resp, err
}
// Create a new tag
func (s *TagsServiceOp) Create(createRequest *TagCreateRequest) (*Tag, *Response, error) {
if createRequest == nil {
return nil, nil, NewArgError("createRequest", "cannot be nil")
}
req, err := s.client.NewRequest("POST", tagsBasePath, createRequest)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.Tag, resp, err
}
// Update an exsting tag
func (s *TagsServiceOp) Update(name string, updateRequest *TagUpdateRequest) (*Response, error) {
if name == "" {
return nil, NewArgError("name", "cannot be empty")
}
if updateRequest == nil {
return nil, NewArgError("updateRequest", "cannot be nil")
}
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
req, err := s.client.NewRequest("PUT", path, updateRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Delete an existing tag
func (s *TagsServiceOp) Delete(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("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Associate resources with a tag
func (s *TagsServiceOp) TagResources(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("POST", path, tagRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Dissociate resources with a tag
func (s *TagsServiceOp) UntagResources(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("DELETE", path, untagRequest)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}

4
vendor/vendor.json vendored
View File

@ -625,9 +625,11 @@
"revisionTime": "2016-06-17T17:01:58Z"
},
{
"checksumSHA1": "mbMr6wMbQnMrfIwUtej8QcGsx0A=",
"comment": "v0.9.0-20-gf75d769",
"path": "github.com/digitalocean/godo",
"revision": "f75d769b07edce8a73682dcf325b4404f366ab3d"
"revision": "e03ac28c3d9b216f7e9ed16bc6aa39e344d56491",
"revisionTime": "2016-06-27T19:55:12Z"
},
{
"path": "github.com/dylanmei/iso8601",

View File

@ -44,6 +44,8 @@ The following arguments are supported:
the format `[12345, 123456]`. To retrieve this info, use a tool such
as `curl` with the [DigitalOcean API](https://developers.digitalocean.com/#keys),
to retrieve them.
* `tags` - (Optional) A list of the tags to label this droplet. A tag resource
must exist before it can be associated with a droplet.
* `user_data` (Optional) - A string of the desired User Data for the Droplet.
User Data is currently only available in regions with metadata
listed in their features.
@ -65,4 +67,4 @@ The following attributes are exported:
* `private_networking` - Is private networking enabled
* `size` - The instance size
* `status` - The status of the droplet
* `tags` - The tags associated with the droplet

View File

@ -0,0 +1,36 @@
---
layout: "digitalocean"
page_title: "DigitalOcean: digitalocean_tag"
sidebar_current: "docs-do-resource-tag"
description: |-
Provides a DigitalOcean Tag resource.
---
# digitalocean\_tag
Provides a DigitalOcean Tag resource. A Tag is a label that can be applied to a
droplet resource in order to better organize or facilitate the lookups and
actions on it. Tags created with this resource can be referenced in your droplet
configuration via their ID or name.
## Example Usage
```
# Create a new SSH key
resource "digitalocean_tag" "default" {
name = "foobar"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the tag
## Attributes Reference
The following attributes are exported:
* `id` - The name of the tag
* `name` - The name of the tag