Skip to content

Commit 3d6fe76

Browse files
aybabtmestack72
authored andcommitted
provider/digitalocean: adds a volume resource (hashicorp#7560)
* provider/digitalocean: add support for volumes * provider/digitalocean: add documentation for volume resource
1 parent 9018e07 commit 3d6fe76

9 files changed

Lines changed: 489 additions & 12 deletions

File tree

builtin/providers/digitalocean/config.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package digitalocean
22

33
import (
44
"log"
5+
"time"
56

67
"github.com/digitalocean/godo"
8+
"github.com/hashicorp/terraform/helper/resource"
79
"golang.org/x/oauth2"
810
)
911

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

2426
return client, nil
2527
}
28+
29+
// waitForAction waits for the action to finish using the resource.StateChangeConf.
30+
func waitForAction(client *godo.Client, action *godo.Action) error {
31+
var (
32+
pending = "in-progress"
33+
target = "completed"
34+
refreshfn = func() (result interface{}, state string, err error) {
35+
a, _, err := client.Actions.Get(action.ID)
36+
if err != nil {
37+
return nil, "", err
38+
}
39+
if a.Status == "errored" {
40+
return a, "errored", nil
41+
}
42+
if a.CompletedAt != nil {
43+
return a, target, nil
44+
}
45+
return a, pending, nil
46+
}
47+
)
48+
_, err := (&resource.StateChangeConf{
49+
Pending: []string{pending},
50+
Refresh: refreshfn,
51+
Target: []string{target},
52+
53+
Delay: 10 * time.Second,
54+
Timeout: 60 * time.Minute,
55+
MinTimeout: 3 * time.Second,
56+
57+
// This is a hack around DO API strangeness.
58+
// https://github.com/hashicorp/terraform/issues/481
59+
//
60+
NotFoundChecks: 60,
61+
}).WaitForState()
62+
return err
63+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package digitalocean
2+
3+
import (
4+
"testing"
5+
6+
"fmt"
7+
8+
"github.com/hashicorp/terraform/helper/acctest"
9+
"github.com/hashicorp/terraform/helper/resource"
10+
)
11+
12+
func TestAccDigitalOceanVolume_importBasic(t *testing.T) {
13+
resourceName := "digitalocean_volume.foobar"
14+
volumeName := fmt.Sprintf("volume-%s", acctest.RandString(10))
15+
16+
resource.Test(t, resource.TestCase{
17+
PreCheck: func() { testAccPreCheck(t) },
18+
Providers: testAccProviders,
19+
CheckDestroy: testAccCheckDigitalOceanVolumeDestroy,
20+
Steps: []resource.TestStep{
21+
resource.TestStep{
22+
Config: fmt.Sprintf(testAccCheckDigitalOceanVolumeConfig_basic, volumeName),
23+
},
24+
25+
resource.TestStep{
26+
ResourceName: resourceName,
27+
ImportState: true,
28+
ImportStateVerify: true,
29+
},
30+
},
31+
})
32+
}

builtin/providers/digitalocean/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func Provider() terraform.ResourceProvider {
2424
"digitalocean_record": resourceDigitalOceanRecord(),
2525
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),
2626
"digitalocean_tag": resourceDigitalOceanTag(),
27+
"digitalocean_volume": resourceDigitalOceanVolume(),
2728
},
2829

2930
ConfigureFunc: providerConfigure,

builtin/providers/digitalocean/resource_digitalocean_droplet.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ func resourceDigitalOceanDroplet() *schema.Resource {
115115
Optional: true,
116116
ForceNew: true,
117117
},
118+
119+
"volume_ids": &schema.Schema{
120+
Type: schema.TypeList,
121+
Elem: &schema.Schema{Type: schema.TypeString},
122+
Optional: true,
123+
},
118124
},
119125
}
120126
}
@@ -148,6 +154,14 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{})
148154
opts.UserData = attr.(string)
149155
}
150156

157+
if attr, ok := d.GetOk("volume_ids"); ok {
158+
for _, id := range attr.([]interface{}) {
159+
opts.Volumes = append(opts.Volumes, godo.DropletCreateVolume{
160+
ID: id.(string),
161+
})
162+
}
163+
}
164+
151165
// Get configured ssh_keys
152166
sshKeys := d.Get("ssh_keys.#").(int)
153167
if sshKeys > 0 {
@@ -229,6 +243,14 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e
229243
d.Set("status", droplet.Status)
230244
d.Set("locked", strconv.FormatBool(droplet.Locked))
231245

246+
if len(droplet.VolumeIDs) > 0 {
247+
vlms := make([]interface{}, 0, len(droplet.VolumeIDs))
248+
for _, vid := range droplet.VolumeIDs {
249+
vlms = append(vlms, vid)
250+
}
251+
d.Set("volume_ids", vlms)
252+
}
253+
232254
if publicIPv6 := findIPv6AddrByType(droplet, "public"); publicIPv6 != "" {
233255
d.Set("ipv6", true)
234256
d.Set("ipv6_address", publicIPv6)
@@ -400,6 +422,49 @@ func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{})
400422
}
401423
}
402424

425+
if d.HasChange("volume_ids") {
426+
oldIDs, newIDs := d.GetChange("volume_ids")
427+
newSet := func(ids []interface{}) map[string]struct{} {
428+
out := make(map[string]struct{}, len(ids))
429+
for _, id := range ids {
430+
out[id.(string)] = struct{}{}
431+
}
432+
return out
433+
}
434+
// leftDiff returns all elements in Left that are not in Right
435+
leftDiff := func(left, right map[string]struct{}) map[string]struct{} {
436+
out := make(map[string]struct{})
437+
for l := range left {
438+
if _, ok := right[l]; !ok {
439+
out[l] = struct{}{}
440+
}
441+
}
442+
return out
443+
}
444+
oldIDSet := newSet(oldIDs.([]interface{}))
445+
newIDSet := newSet(newIDs.([]interface{}))
446+
for volumeID := range leftDiff(newIDSet, oldIDSet) {
447+
action, _, err := client.StorageActions.Attach(volumeID, id)
448+
if err != nil {
449+
return fmt.Errorf("Error attaching volume %q to droplet (%s): %s", volumeID, d.Id(), err)
450+
}
451+
// can't fire >1 action at a time, so waiting for each is OK
452+
if err := waitForAction(client, action); err != nil {
453+
return fmt.Errorf("Error waiting for volume %q to attach to droplet (%s): %s", volumeID, d.Id(), err)
454+
}
455+
}
456+
for volumeID := range leftDiff(oldIDSet, newIDSet) {
457+
action, _, err := client.StorageActions.Detach(volumeID)
458+
if err != nil {
459+
return fmt.Errorf("Error detaching volume %q from droplet (%s): %s", volumeID, d.Id(), err)
460+
}
461+
// can't fire >1 action at a time, so waiting for each is OK
462+
if err := waitForAction(client, action); err != nil {
463+
return fmt.Errorf("Error waiting for volume %q to detach from droplet (%s): %s", volumeID, d.Id(), err)
464+
}
465+
}
466+
}
467+
403468
return resourceDigitalOceanDropletRead(d, meta)
404469
}
405470

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package digitalocean
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"github.com/digitalocean/godo"
8+
"github.com/hashicorp/terraform/helper/schema"
9+
)
10+
11+
func resourceDigitalOceanVolume() *schema.Resource {
12+
return &schema.Resource{
13+
Create: resourceDigitalOceanVolumeCreate,
14+
Read: resourceDigitalOceanVolumeRead,
15+
Delete: resourceDigitalOceanVolumeDelete,
16+
Importer: &schema.ResourceImporter{
17+
State: resourceDigitalOceanVolumeImport,
18+
},
19+
20+
Schema: map[string]*schema.Schema{
21+
"region": &schema.Schema{
22+
Type: schema.TypeString,
23+
Required: true,
24+
ForceNew: true,
25+
},
26+
27+
"id": &schema.Schema{
28+
Type: schema.TypeString,
29+
Computed: true,
30+
},
31+
32+
"name": &schema.Schema{
33+
Type: schema.TypeString,
34+
Required: true,
35+
ForceNew: true,
36+
},
37+
38+
"droplet_ids": &schema.Schema{
39+
Type: schema.TypeSet,
40+
Elem: &schema.Schema{Type: schema.TypeInt},
41+
Computed: true,
42+
},
43+
44+
"size": &schema.Schema{
45+
Type: schema.TypeInt,
46+
Required: true,
47+
ForceNew: true, // Update-ability Coming Soon ™
48+
},
49+
50+
"description": &schema.Schema{
51+
Type: schema.TypeString,
52+
Optional: true,
53+
ForceNew: true, // Update-ability Coming Soon ™
54+
},
55+
},
56+
}
57+
}
58+
59+
func resourceDigitalOceanVolumeCreate(d *schema.ResourceData, meta interface{}) error {
60+
client := meta.(*godo.Client)
61+
62+
opts := &godo.VolumeCreateRequest{
63+
Region: d.Get("region").(string),
64+
Name: d.Get("name").(string),
65+
Description: d.Get("description").(string),
66+
SizeGigaBytes: int64(d.Get("size").(int)),
67+
}
68+
69+
log.Printf("[DEBUG] Volume create configuration: %#v", opts)
70+
volume, _, err := client.Storage.CreateVolume(opts)
71+
if err != nil {
72+
return fmt.Errorf("Error creating Volume: %s", err)
73+
}
74+
75+
d.SetId(volume.ID)
76+
log.Printf("[INFO] Volume name: %s", volume.Name)
77+
78+
return resourceDigitalOceanVolumeRead(d, meta)
79+
}
80+
81+
func resourceDigitalOceanVolumeRead(d *schema.ResourceData, meta interface{}) error {
82+
client := meta.(*godo.Client)
83+
84+
volume, resp, err := client.Storage.GetVolume(d.Id())
85+
if err != nil {
86+
// If the volume is somehow already destroyed, mark as
87+
// successfully gone
88+
if resp.StatusCode == 404 {
89+
d.SetId("")
90+
return nil
91+
}
92+
93+
return fmt.Errorf("Error retrieving volume: %s", err)
94+
}
95+
96+
d.Set("id", volume.ID)
97+
98+
dids := make([]interface{}, 0, len(volume.DropletIDs))
99+
for _, did := range volume.DropletIDs {
100+
dids = append(dids, did)
101+
}
102+
d.Set("droplet_ids", schema.NewSet(
103+
func(dropletID interface{}) int { return dropletID.(int) },
104+
dids,
105+
))
106+
107+
return nil
108+
}
109+
110+
func resourceDigitalOceanVolumeDelete(d *schema.ResourceData, meta interface{}) error {
111+
client := meta.(*godo.Client)
112+
113+
log.Printf("[INFO] Deleting volume: %s", d.Id())
114+
_, err := client.Storage.DeleteVolume(d.Id())
115+
if err != nil {
116+
return fmt.Errorf("Error deleting volume: %s", err)
117+
}
118+
119+
d.SetId("")
120+
return nil
121+
}
122+
123+
func resourceDigitalOceanVolumeImport(rs *schema.ResourceData, v interface{}) ([]*schema.ResourceData, error) {
124+
client := v.(*godo.Client)
125+
volume, _, err := client.Storage.GetVolume(rs.Id())
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
rs.Set("id", volume.ID)
131+
rs.Set("name", volume.Name)
132+
rs.Set("region", volume.Region.Slug)
133+
rs.Set("description", volume.Description)
134+
rs.Set("size", int(volume.SizeGigaBytes))
135+
136+
dids := make([]interface{}, 0, len(volume.DropletIDs))
137+
for _, did := range volume.DropletIDs {
138+
dids = append(dids, did)
139+
}
140+
rs.Set("droplet_ids", schema.NewSet(
141+
func(dropletID interface{}) int { return dropletID.(int) },
142+
dids,
143+
))
144+
145+
return []*schema.ResourceData{rs}, nil
146+
}

0 commit comments

Comments
 (0)