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 d612f328aa
commit d371bf4e42
7 changed files with 397 additions and 0 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"
}`)

72
tags.go Normal file
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
}

51
tags_test.go Normal file
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)
}
}
}