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:
parent
d612f328aa
commit
d371bf4e42
|
@ -23,6 +23,7 @@ func Provider() terraform.ResourceProvider {
|
|||
"digitalocean_floating_ip": resourceDigitalOceanFloatingIp(),
|
||||
"digitalocean_record": resourceDigitalOceanRecord(),
|
||||
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),
|
||||
"digitalocean_tag": resourceDigitalOceanTag(),
|
||||
},
|
||||
|
||||
ConfigureFunc: providerConfigure,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}`)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue