From 0c6856f85c1703f0c0b9191b0cb111640e86b5bc Mon Sep 17 00:00:00 2001 From: Tommy Murphy Date: Mon, 11 Jul 2016 07:09:06 -0400 Subject: [PATCH] digitalocean tag support (#7500) * vendor: update godo to support tags * digitalocean: introduce tag resource * website: update for digitalocean_tag resource --- builtin/providers/digitalocean/provider.go | 1 + .../resource_digitalocean_droplet.go | 21 ++ .../resource_digitalocean_droplet_test.go | 55 ++++ .../digitalocean/resource_digitalocean_tag.go | 104 ++++++++ .../resource_digitalocean_tag_test.go | 93 +++++++ builtin/providers/digitalocean/tags.go | 72 +++++ builtin/providers/digitalocean/tags_test.go | 51 ++++ .../github.com/digitalocean/godo/.travis.yml | 6 - .../digitalocean/godo/droplet_actions.go | 96 +++++++ .../github.com/digitalocean/godo/droplets.go | 145 ++++++++-- vendor/github.com/digitalocean/godo/godo.go | 59 +++- .../github.com/digitalocean/godo/storage.go | 252 ++++++++++++++++++ .../digitalocean/godo/storage_actions.go | 61 +++++ vendor/github.com/digitalocean/godo/tags.go | 226 ++++++++++++++++ vendor/vendor.json | 4 +- .../docs/providers/do/r/droplet.html.markdown | 4 +- .../docs/providers/do/r/tag.html.markdown | 36 +++ 17 files changed, 1253 insertions(+), 33 deletions(-) create mode 100644 builtin/providers/digitalocean/resource_digitalocean_tag.go create mode 100644 builtin/providers/digitalocean/resource_digitalocean_tag_test.go create mode 100644 builtin/providers/digitalocean/tags.go create mode 100644 builtin/providers/digitalocean/tags_test.go delete mode 100644 vendor/github.com/digitalocean/godo/.travis.yml create mode 100644 vendor/github.com/digitalocean/godo/storage.go create mode 100644 vendor/github.com/digitalocean/godo/storage_actions.go create mode 100644 vendor/github.com/digitalocean/godo/tags.go create mode 100644 website/source/docs/providers/do/r/tag.html.markdown diff --git a/builtin/providers/digitalocean/provider.go b/builtin/providers/digitalocean/provider.go index be197a32f..3e8771212 100644 --- a/builtin/providers/digitalocean/provider.go +++ b/builtin/providers/digitalocean/provider.go @@ -23,6 +23,7 @@ func Provider() terraform.ResourceProvider { "digitalocean_floating_ip": resourceDigitalOceanFloatingIp(), "digitalocean_record": resourceDigitalOceanRecord(), "digitalocean_ssh_key": resourceDigitalOceanSSHKey(), + "digitalocean_tag": resourceDigitalOceanTag(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet.go b/builtin/providers/digitalocean/resource_digitalocean_droplet.go index 2e07e0082..ae22b9131 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet.go @@ -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) } diff --git a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go index 3a72e3c5d..23485cfd6 100644 --- a/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go +++ b/builtin/providers/digitalocean/resource_digitalocean_droplet_test.go @@ -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" diff --git a/builtin/providers/digitalocean/resource_digitalocean_tag.go b/builtin/providers/digitalocean/resource_digitalocean_tag.go new file mode 100644 index 000000000..2980d29e8 --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_tag.go @@ -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 +} diff --git a/builtin/providers/digitalocean/resource_digitalocean_tag_test.go b/builtin/providers/digitalocean/resource_digitalocean_tag_test.go new file mode 100644 index 000000000..932c3d4c7 --- /dev/null +++ b/builtin/providers/digitalocean/resource_digitalocean_tag_test.go @@ -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" +}`) diff --git a/builtin/providers/digitalocean/tags.go b/builtin/providers/digitalocean/tags.go new file mode 100644 index 000000000..6e952cef4 --- /dev/null +++ b/builtin/providers/digitalocean/tags.go @@ -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 +} diff --git a/builtin/providers/digitalocean/tags_test.go b/builtin/providers/digitalocean/tags_test.go new file mode 100644 index 000000000..02686ed2d --- /dev/null +++ b/builtin/providers/digitalocean/tags_test.go @@ -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) + } + } +} diff --git a/vendor/github.com/digitalocean/godo/.travis.yml b/vendor/github.com/digitalocean/godo/.travis.yml deleted file mode 100644 index 245a2f517..000000000 --- a/vendor/github.com/digitalocean/godo/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: go - -go: - - 1.3 - - 1.4 - - tip diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go index 7012aee7f..c01ba36a0 100644 --- a/vendor/github.com/digitalocean/godo/droplet_actions.go +++ b/vendor/github.com/digitalocean/godo/droplet_actions.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go index 978d41e39..c17bc3bc5 100644 --- a/vendor/github.com/digitalocean/godo/droplets.go +++ b/vendor/github.com/digitalocean/godo/droplets.go @@ -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"` + 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 { diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go index f57b3bca6..78fc8be27 100644 --- a/vendor/github.com/digitalocean/godo/godo.go +++ b/vendor/github.com/digitalocean/godo/godo.go @@ -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) } diff --git a/vendor/github.com/digitalocean/godo/storage.go b/vendor/github.com/digitalocean/godo/storage.go new file mode 100644 index 000000000..5667ff7bd --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/storage_actions.go b/vendor/github.com/digitalocean/godo/storage_actions.go new file mode 100644 index 000000000..20dc4aa59 --- /dev/null +++ b/vendor/github.com/digitalocean/godo/storage_actions.go @@ -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) +} diff --git a/vendor/github.com/digitalocean/godo/tags.go b/vendor/github.com/digitalocean/godo/tags.go new file mode 100644 index 000000000..dd6c638ec --- /dev/null +++ b/vendor/github.com/digitalocean/godo/tags.go @@ -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 +} diff --git a/vendor/vendor.json b/vendor/vendor.json index d9635eb2a..205d5b5a1 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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", diff --git a/website/source/docs/providers/do/r/droplet.html.markdown b/website/source/docs/providers/do/r/droplet.html.markdown index 053ab74a1..f9aab4f68 100644 --- a/website/source/docs/providers/do/r/droplet.html.markdown +++ b/website/source/docs/providers/do/r/droplet.html.markdown @@ -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 diff --git a/website/source/docs/providers/do/r/tag.html.markdown b/website/source/docs/providers/do/r/tag.html.markdown new file mode 100644 index 000000000..562e400c6 --- /dev/null +++ b/website/source/docs/providers/do/r/tag.html.markdown @@ -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