Skip to content

Commit

Permalink
provider/digitalocean: adds a volume resource (hashicorp#7560)
Browse files Browse the repository at this point in the history
* provider/digitalocean: add support for volumes

* provider/digitalocean: add documentation for volume resource
  • Loading branch information
aybabtme authored and Andy Chan committed Jul 22, 2016
1 parent 1c4cf3f commit 6d2f5b9
Show file tree
Hide file tree
Showing 9 changed files with 489 additions and 12 deletions.
38 changes: 38 additions & 0 deletions builtin/providers/digitalocean/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package digitalocean

import (
"log"
"time"

"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/resource"
"golang.org/x/oauth2"
)

Expand All @@ -23,3 +25,39 @@ func (c *Config) Client() (*godo.Client, error) {

return client, nil
}

// waitForAction waits for the action to finish using the resource.StateChangeConf.
func waitForAction(client *godo.Client, action *godo.Action) error {
var (
pending = "in-progress"
target = "completed"
refreshfn = func() (result interface{}, state string, err error) {
a, _, err := client.Actions.Get(action.ID)
if err != nil {
return nil, "", err
}
if a.Status == "errored" {
return a, "errored", nil
}
if a.CompletedAt != nil {
return a, target, nil
}
return a, pending, nil
}
)
_, err := (&resource.StateChangeConf{
Pending: []string{pending},
Refresh: refreshfn,
Target: []string{target},

Delay: 10 * time.Second,
Timeout: 60 * time.Minute,
MinTimeout: 3 * time.Second,

// This is a hack around DO API strangeness.
// https://github.com/hashicorp/terraform/issues/481
//
NotFoundChecks: 60,
}).WaitForState()
return err
}
32 changes: 32 additions & 0 deletions builtin/providers/digitalocean/import_digitalocean_volume_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package digitalocean

import (
"testing"

"fmt"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)

func TestAccDigitalOceanVolume_importBasic(t *testing.T) {
resourceName := "digitalocean_volume.foobar"
volumeName := fmt.Sprintf("volume-%s", acctest.RandString(10))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanVolumeDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: fmt.Sprintf(testAccCheckDigitalOceanVolumeConfig_basic, volumeName),
},

resource.TestStep{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
1 change: 1 addition & 0 deletions builtin/providers/digitalocean/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func Provider() terraform.ResourceProvider {
"digitalocean_record": resourceDigitalOceanRecord(),
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),
"digitalocean_tag": resourceDigitalOceanTag(),
"digitalocean_volume": resourceDigitalOceanVolume(),
},

ConfigureFunc: providerConfigure,
Expand Down
65 changes: 65 additions & 0 deletions builtin/providers/digitalocean/resource_digitalocean_droplet.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ func resourceDigitalOceanDroplet() *schema.Resource {
Optional: true,
ForceNew: true,
},

"volume_ids": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -148,6 +154,14 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{})
opts.UserData = attr.(string)
}

if attr, ok := d.GetOk("volume_ids"); ok {
for _, id := range attr.([]interface{}) {
opts.Volumes = append(opts.Volumes, godo.DropletCreateVolume{
ID: id.(string),
})
}
}

// Get configured ssh_keys
sshKeys := d.Get("ssh_keys.#").(int)
if sshKeys > 0 {
Expand Down Expand Up @@ -229,6 +243,14 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e
d.Set("status", droplet.Status)
d.Set("locked", strconv.FormatBool(droplet.Locked))

if len(droplet.VolumeIDs) > 0 {
vlms := make([]interface{}, 0, len(droplet.VolumeIDs))
for _, vid := range droplet.VolumeIDs {
vlms = append(vlms, vid)
}
d.Set("volume_ids", vlms)
}

if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" {
d.Set("ipv6", true)
d.Set("ipv6_address", publicIPv6)
Expand Down Expand Up @@ -400,6 +422,49 @@ func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{})
}
}

if d.HasChange("volume_ids") {
oldIDs, newIDs := d.GetChange("volume_ids")
newSet := func(ids []interface{}) map[string]struct{} {
out := make(map[string]struct{}, len(ids))
for _, id := range ids {
out[id.(string)] = struct{}{}
}
return out
}
// leftDiff returns all elements in Left that are not in Right
leftDiff := func(left, right map[string]struct{}) map[string]struct{} {
out := make(map[string]struct{})
for l := range left {
if _, ok := right[l]; !ok {
out[l] = struct{}{}
}
}
return out
}
oldIDSet := newSet(oldIDs.([]interface{}))
newIDSet := newSet(newIDs.([]interface{}))
for volumeID := range leftDiff(newIDSet, oldIDSet) {
action, _, err := client.StorageActions.Attach(volumeID, id)
if err != nil {
return fmt.Errorf("Error attaching volume %q to droplet (%s): %s", volumeID, d.Id(), err)
}
// can't fire >1 action at a time, so waiting for each is OK
if err := waitForAction(client, action); err != nil {
return fmt.Errorf("Error waiting for volume %q to attach to droplet (%s): %s", volumeID, d.Id(), err)
}
}
for volumeID := range leftDiff(oldIDSet, newIDSet) {
action, _, err := client.StorageActions.Detach(volumeID)
if err != nil {
return fmt.Errorf("Error detaching volume %q from droplet (%s): %s", volumeID, d.Id(), err)
}
// can't fire >1 action at a time, so waiting for each is OK
if err := waitForAction(client, action); err != nil {
return fmt.Errorf("Error waiting for volume %q to detach from droplet (%s): %s", volumeID, d.Id(), err)
}
}
}

return resourceDigitalOceanDropletRead(d, meta)
}

Expand Down
146 changes: 146 additions & 0 deletions builtin/providers/digitalocean/resource_digitalocean_volume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package digitalocean

import (
"fmt"
"log"

"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceDigitalOceanVolume() *schema.Resource {
return &schema.Resource{
Create: resourceDigitalOceanVolumeCreate,
Read: resourceDigitalOceanVolumeRead,
Delete: resourceDigitalOceanVolumeDelete,
Importer: &schema.ResourceImporter{
State: resourceDigitalOceanVolumeImport,
},

Schema: map[string]*schema.Schema{
"region": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},

"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"droplet_ids": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeInt},
Computed: true,
},

"size": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true, // Update-ability Coming Soon ™
},

"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true, // Update-ability Coming Soon ™
},
},
}
}

func resourceDigitalOceanVolumeCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)

opts := &godo.VolumeCreateRequest{
Region: d.Get("region").(string),
Name: d.Get("name").(string),
Description: d.Get("description").(string),
SizeGigaBytes: int64(d.Get("size").(int)),
}

log.Printf("[DEBUG] Volume create configuration: %#v", opts)
volume, _, err := client.Storage.CreateVolume(opts)
if err != nil {
return fmt.Errorf("Error creating Volume: %s", err)
}

d.SetId(volume.ID)
log.Printf("[INFO] Volume name: %s", volume.Name)

return resourceDigitalOceanVolumeRead(d, meta)
}

func resourceDigitalOceanVolumeRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)

volume, resp, err := client.Storage.GetVolume(d.Id())
if err != nil {
// If the volume is somehow already destroyed, mark as
// successfully gone
if resp.StatusCode == 404 {
d.SetId("")
return nil
}

return fmt.Errorf("Error retrieving volume: %s", err)
}

d.Set("id", volume.ID)

dids := make([]interface{}, 0, len(volume.DropletIDs))
for _, did := range volume.DropletIDs {
dids = append(dids, did)
}
d.Set("droplet_ids", schema.NewSet(
func(dropletID interface{}) int { return dropletID.(int) },
dids,
))

return nil
}

func resourceDigitalOceanVolumeDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)

log.Printf("[INFO] Deleting volume: %s", d.Id())
_, err := client.Storage.DeleteVolume(d.Id())
if err != nil {
return fmt.Errorf("Error deleting volume: %s", err)
}

d.SetId("")
return nil
}

func resourceDigitalOceanVolumeImport(rs *schema.ResourceData, v interface{}) ([]*schema.ResourceData, error) {
client := v.(*godo.Client)
volume, _, err := client.Storage.GetVolume(rs.Id())
if err != nil {
return nil, err
}

rs.Set("id", volume.ID)
rs.Set("name", volume.Name)
rs.Set("region", volume.Region.Slug)
rs.Set("description", volume.Description)
rs.Set("size", int(volume.SizeGigaBytes))

dids := make([]interface{}, 0, len(volume.DropletIDs))
for _, did := range volume.DropletIDs {
dids = append(dids, did)
}
rs.Set("droplet_ids", schema.NewSet(
func(dropletID interface{}) int { return dropletID.(int) },
dids,
))

return []*schema.ResourceData{rs}, nil
}
Loading

0 comments on commit 6d2f5b9

Please sign in to comment.