Added the digitalocean_snapshot resource for creating volume snapshots

This commit is contained in:
Tilen Faganel 2018-09-07 12:34:59 +01:00
parent eefad9be9e
commit 9e5ec3d2a8
No known key found for this signature in database
GPG Key ID: 3DDA5ABF228F8E7A
12 changed files with 677 additions and 271 deletions

View File

@ -27,7 +27,7 @@ func TestAccDigitalOceanImage_Basic(t *testing.T) {
Config: testAccCheckDigitalOceanDropletConfig_basic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet),
takeSnapshotsOfDroplet(rInt, &droplet, false, &snapshotsId),
takeSnapshotsOfDroplet(rInt, &droplet, &snapshotsId),
),
},
{
@ -87,21 +87,13 @@ func TestAccDigitalOceanImage_PublicSlug(t *testing.T) {
})
}
func takeSnapshotsOfDroplet(rInt int, droplet *godo.Droplet, increment bool, snapshotsId *[]int) resource.TestCheckFunc {
func takeSnapshotsOfDroplet(rInt int, droplet *godo.Droplet, snapshotsId *[]int) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)
for i := 0; i < 3; i++ {
switch increment {
case true:
err := takeSnapshotOfDroplet(rInt, i, droplet)
if err != nil {
return err
}
case false:
err := takeSnapshotOfDroplet(rInt, i%2, droplet)
if err != nil {
return err
}
err := takeSnapshotOfDroplet(rInt, i%2, droplet)
if err != nil {
return err
}
}
retrieveDroplet, _, err := client.Droplets.Get(context.Background(), (*droplet).ID)
@ -125,11 +117,13 @@ func takeSnapshotOfDroplet(rInt, sInt int, droplet *godo.Droplet) error {
func deleteDropletSnapshots(snapshotsId *[]int) resource.TestCheckFunc {
return func(s *terraform.State) error {
log.Printf("XXX Deleting Droplet snapshots")
log.Printf("Deleting Droplet snapshots")
client := testAccProvider.Meta().(*godo.Client)
snapshots := *snapshotsId
for _, value := range snapshots {
log.Printf("XXX Deleting %d", value)
log.Printf("Deleting %d", value)
_, err := client.Images.Delete(context.Background(), value)
if err != nil {
return err

View File

@ -10,35 +10,33 @@ import (
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func dataSourceDigitalOceanSnapshot() *schema.Resource {
return &schema.Resource{
Read: dataSourceDoSnapshotRead,
Schema: map[string]*schema.Schema{
"name_regex": {
"name": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validateSnapshotNameRegex,
ValidateFunc: validation.NoZeroValues,
},
"name_regex": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.ValidateRegexp,
ConflictsWith: []string{"name"},
},
"region": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.NoZeroValues,
},
"most_recent": {
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
"region_filter": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"resource_type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateResourceType,
},
// Computed values.
"created_at": {
@ -49,20 +47,16 @@ func dataSourceDigitalOceanSnapshot() *schema.Resource {
Type: schema.TypeInt,
Computed: true,
},
"name": {
Type: schema.TypeString,
Computed: true,
},
"regions": {
Type: schema.TypeList,
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"resource_id": {
"volume_id": {
Type: schema.TypeString,
Computed: true,
},
"size_gigabytes": {
"size": {
Type: schema.TypeFloat,
Computed: true,
},
@ -74,26 +68,26 @@ func dataSourceDigitalOceanSnapshot() *schema.Resource {
func dataSourceDoSnapshotRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
resourceType := d.Get("resource_type")
nameRegex, nameRegexOk := d.GetOk("name_regex")
regionFilter, regionFilterOk := d.GetOk("region_filter")
name, hasName := d.GetOk("name")
nameRegex, hasNameRegex := d.GetOk("name_regex")
region, hasRegion := d.GetOk("region")
pageOpt := &godo.ListOptions{
if !hasName && !hasNameRegex {
return fmt.Errorf("One of `name` or `name_regex` must be assigned")
}
opts := &godo.ListOptions{
Page: 1,
PerPage: 200,
}
var snapshotList []godo.Snapshot
for {
var snapshots []godo.Snapshot
var resp *godo.Response
var err error
switch resourceType {
case "droplet":
snapshots, resp, err = client.Snapshots.ListDroplet(context.Background(), pageOpt)
case "volume":
snapshots, resp, err = client.Snapshots.ListVolume(context.Background(), pageOpt)
for {
snapshots, resp, err := client.Snapshots.ListVolume(context.Background(), opts)
if err != nil {
return fmt.Errorf("Error retrieving snapshots: %s", err)
}
for _, s := range snapshots {
@ -106,114 +100,92 @@ func dataSourceDoSnapshotRead(d *schema.ResourceData, meta interface{}) error {
page, err := resp.Links.CurrentPage()
if err != nil {
return err
return fmt.Errorf("Error retrieving snapshots: %s", err)
}
pageOpt.Page = page + 1
opts.Page = page + 1
}
var snapshotsFilteredByName []godo.Snapshot
if nameRegexOk {
r := regexp.MustCompile(nameRegex.(string))
for _, snapshot := range snapshotList {
if r.MatchString(snapshot.Name) {
snapshotsFilteredByName = append(snapshotsFilteredByName, snapshot)
}
}
// Go through all the possible filters
if hasName {
snapshotList = filterSnapshotsByName(snapshotList, name.(string))
} else {
snapshotsFilteredByName = snapshotList[:]
snapshotList = filterSnapshotsByNameRegex(snapshotList, nameRegex.(string))
}
if hasRegion {
snapshotList = filterSnapshotsByRegion(snapshotList, region.(string))
}
var snapshotsFilteredByRegion []godo.Snapshot
if regionFilterOk {
for _, snapshot := range snapshotsFilteredByName {
for _, region := range snapshot.Regions {
if region == regionFilter {
snapshotsFilteredByRegion = append(snapshotsFilteredByRegion, snapshot)
}
}
}
} else {
snapshotsFilteredByRegion = snapshotsFilteredByName[:]
// Get the queried snapshot or fail if it can't be determined
var snapshot *godo.Snapshot
if len(snapshotList) == 0 {
return fmt.Errorf("no snapshot found with name %s", name)
}
var snapshot godo.Snapshot
if len(snapshotsFilteredByRegion) < 1 {
return fmt.Errorf("Your query returned no results. Please change your search criteria and try again.")
}
recent := d.Get("most_recent").(bool)
if len(snapshotsFilteredByRegion) > 1 {
log.Printf("[DEBUG] do_snapshot - multiple results found and `most_recent` is set to: %t", recent)
if len(snapshotList) > 1 {
recent := d.Get("most_recent").(bool)
if recent {
snapshot = mostRecentSnapshot(snapshotsFilteredByRegion)
snapshot = findMostRecentSnapshot(snapshotList)
} else {
return fmt.Errorf("Your query returned more than one result. Please try a more " +
"specific search criteria, or set `most_recent` attribute to true.")
return fmt.Errorf("too many snapshots found with name %s (found %d, expected 1)", name, len(snapshotList))
}
} else {
// Query returned single result.
snapshot = snapshotsFilteredByRegion[0]
snapshot = &snapshotList[0]
}
log.Printf("[DEBUG] do_snapshot - Single Snapshot found: %s", snapshot.ID)
return snapshotDescriptionAttributes(d, snapshot)
}
type snapshotSort []godo.Snapshot
func (a snapshotSort) Len() int { return len(a) }
func (a snapshotSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a snapshotSort) Less(i, j int) bool {
itime, _ := time.Parse(time.RFC3339, a[i].Created)
jtime, _ := time.Parse(time.RFC3339, a[j].Created)
return itime.Unix() < jtime.Unix()
}
// Returns the most recent Snapshot out of a slice of Snapshots.
func mostRecentSnapshot(snapshots []godo.Snapshot) godo.Snapshot {
sortedSnapshots := snapshots
sort.Sort(snapshotSort(sortedSnapshots))
return sortedSnapshots[len(sortedSnapshots)-1]
}
// populate the numerous fields that the Snapshot description returns.
func snapshotDescriptionAttributes(d *schema.ResourceData, snapshot godo.Snapshot) error {
d.SetId(snapshot.ID)
d.Set("name", snapshot.Name)
d.Set("created_at", snapshot.Created)
d.Set("min_disk_size", snapshot.MinDiskSize)
d.Set("name", snapshot.Name)
d.Set("regions", snapshot.Regions)
d.Set("resource_id", snapshot.ResourceID)
d.Set("size_gigabytes", snapshot.SizeGigaBytes)
d.Set("volume_id", snapshot.ResourceID)
d.Set("size", snapshot.SizeGigaBytes)
return nil
}
func validateSnapshotNameRegex(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if _, err := regexp.Compile(value); err != nil {
errors = append(errors, fmt.Errorf(
"%q contains an invalid regular expression: %s",
k, err))
func filterSnapshotsByName(snapshots []godo.Snapshot, name string) []godo.Snapshot {
result := make([]godo.Snapshot, 0)
for _, s := range snapshots {
if s.Name == name {
result = append(result, s)
}
}
return
return result
}
func validateResourceType(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
switch value {
case
"droplet",
"volume":
return
func filterSnapshotsByNameRegex(snapshots []godo.Snapshot, name string) []godo.Snapshot {
r := regexp.MustCompile(name)
result := make([]godo.Snapshot, 0)
for _, s := range snapshots {
if r.MatchString(s.Name) {
result = append(result, s)
}
}
errors = append(errors, fmt.Errorf(
"Invalid %q specified: %s",
k, value))
return
return result
}
func filterSnapshotsByRegion(snapshots []godo.Snapshot, region string) []godo.Snapshot {
result := make([]godo.Snapshot, 0)
for _, s := range snapshots {
for _, r := range s.Regions {
if r == region {
result = append(result, s)
break
}
}
}
return result
}
// Returns the most recent Snapshot out of a slice of Snapshots.
func findMostRecentSnapshot(snapshots []godo.Snapshot) *godo.Snapshot {
sort.Slice(snapshots, func(i, j int) bool {
itime, _ := time.Parse(time.RFC3339, snapshots[i].Created)
jtime, _ := time.Parse(time.RFC3339, snapshots[j].Created)
return itime.Unix() > jtime.Unix()
})
return &snapshots[0]
}

View File

@ -3,8 +3,6 @@ package digitalocean
import (
"context"
"fmt"
"log"
"regexp"
"testing"
"github.com/digitalocean/godo"
@ -13,152 +11,166 @@ import (
"github.com/hashicorp/terraform/terraform"
)
func TestAccDigitalOceanSnapshotDataSource_droplet(t *testing.T) {
var (
droplet godo.Droplet
dropletSnapshotsIds []int
)
func TestAccDataSourceDigitalOceanSnapshot_basic(t *testing.T) {
var snapshot godo.Snapshot
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanDropletDestroy,
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanDropletConfig_basic(rInt),
Config: fmt.Sprintf(testAccCheckDataSourceDigitalOceanSnapshot_basic, rInt, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet),
takeSnapshotsOfDroplet(rInt, &droplet, true, &dropletSnapshotsIds),
),
},
{
Config: testAccCheckDigitalOceanDropletSnapshotDataSourceConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.digitalocean_snapshot.droplet_test_snap", "name", fmt.Sprintf("snap-%d-2", rInt)),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.droplet_test_snap", "min_disk_size", "20"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.droplet_test_snap", "regions.0", "nyc3"),
),
},
{
Config: testAccCheckDigitalOceanDropletSnapshotDataSourceConfig_fail,
ExpectError: regexp.MustCompile(`.*Your query returned more than one result.*`),
},
{
Config: " ",
Check: resource.ComposeTestCheckFunc(
deleteDropletSnapshots(&dropletSnapshotsIds),
testAccCheckDataSourceDigitalOceanSnapshotExists("data.digitalocean_snapshot.foobar", &snapshot),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "name", fmt.Sprintf("snapshot-%d", rInt)),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "size", "0"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "min_disk_size", "100"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "regions.#", "1"),
resource.TestCheckResourceAttrSet("data.digitalocean_snapshot.foobar", "volume_id"),
),
},
},
})
}
func TestAccDigitalOceanSnapshotDataSource_volume(t *testing.T) {
var volumeSnapshotsIds []string
func TestAccDataSourceDigitalOceanSnapshot_regex(t *testing.T) {
var snapshot godo.Snapshot
rInt := acctest.RandInt()
name := fmt.Sprintf("volume-%v", rInt)
volume := godo.Volume{
Name: name,
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanVolumeDestroy,
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccCheckDigitalOceanVolumeConfig_basic, name),
Config: fmt.Sprintf(testAccCheckDataSourceDigitalOceanSnapshot_regex, rInt, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanVolumeExists("digitalocean_volume.foobar", &volume),
takeSnapshotsOfVolume(rInt, &volume, &volumeSnapshotsIds),
),
},
{
Config: testAccCheckDigitalOceanVolumeSnapshotDataSourceConfig,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.digitalocean_snapshot.volume_test_snap", "name", fmt.Sprintf("vol-snap-%d-2", rInt)),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.volume_test_snap", "min_disk_size", "100"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.volume_test_snap", "regions.0", "nyc1"),
),
},
{
Config: testAccCheckDigitalOceanVolumeSnapshotDataSourceConfig_fail,
ExpectError: regexp.MustCompile(`.*Your query returned more than one result.*`),
},
{
Config: " ",
Check: resource.ComposeTestCheckFunc(
deleteVolumeSnapshots(&volumeSnapshotsIds),
testAccCheckDataSourceDigitalOceanSnapshotExists("data.digitalocean_snapshot.foobar", &snapshot),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "name", fmt.Sprintf("snapshot-%d", rInt)),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "size", "0"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "min_disk_size", "100"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "regions.#", "1"),
resource.TestCheckResourceAttrSet("data.digitalocean_snapshot.foobar", "volume_id"),
),
},
},
})
}
func takeSnapshotsOfVolume(rInt int, volume *godo.Volume, snapshotsId *[]string) resource.TestCheckFunc {
func TestAccDataSourceDigitalOceanSnapshot_region(t *testing.T) {
var snapshot godo.Snapshot
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccCheckDataSourceDigitalOceanSnapshot_region, rInt, rInt, rInt, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDataSourceDigitalOceanSnapshotExists("data.digitalocean_snapshot.foobar", &snapshot),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "name", fmt.Sprintf("snapshot-%d", rInt)),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "size", "0"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "min_disk_size", "100"),
resource.TestCheckResourceAttr("data.digitalocean_snapshot.foobar", "regions.#", "1"),
resource.TestCheckResourceAttrSet("data.digitalocean_snapshot.foobar", "volume_id"),
),
},
},
})
}
func testAccCheckDataSourceDigitalOceanSnapshotExists(n string, snapshot *godo.Snapshot) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)
for i := 0; i < 3; i++ {
createRequest := &godo.SnapshotCreateRequest{
VolumeID: (*volume).ID,
Name: fmt.Sprintf("vol-snap-%d-%d", rInt, i),
}
volume, _, err := client.Storage.CreateSnapshot(context.Background(), createRequest)
if err != nil {
return err
}
*snapshotsId = append(*snapshotsId, volume.ID)
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No snapshot ID is set")
}
foundSnapshot, _, err := client.Snapshots.Get(context.Background(), rs.Primary.ID)
if err != nil {
return err
}
if foundSnapshot.ID != rs.Primary.ID {
return fmt.Errorf("Snapshot not found")
}
*snapshot = *foundSnapshot
return nil
}
}
func deleteVolumeSnapshots(snapshotsId *[]string) resource.TestCheckFunc {
return func(s *terraform.State) error {
log.Printf("XXX Deleting volume snapshots")
client := testAccProvider.Meta().(*godo.Client)
snapshots := *snapshotsId
for _, value := range snapshots {
log.Printf("XXX Deleting %v", value)
_, err := client.Snapshots.Delete(context.Background(), value)
if err != nil {
return err
}
}
return nil
}
const testAccCheckDataSourceDigitalOceanSnapshot_basic = `
resource "digitalocean_volume" "foo" {
region = "nyc1"
name = "volume-%d"
size = 100
description = "peace makes plenty"
}
const testAccCheckDigitalOceanDropletSnapshotDataSourceConfig = `
data "digitalocean_snapshot" "droplet_test_snap" {
resource "digitalocean_snapshot" "foo" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.foo.id}"
}
data "digitalocean_snapshot" "foobar" {
most_recent = true
resource_type = "droplet"
name_regex = "^snap"
}
`
name = "${digitalocean_snapshot.foo.name}"
}`
const testAccCheckDigitalOceanDropletSnapshotDataSourceConfig_fail = `
data "digitalocean_snapshot" "droplet_test_snap" {
most_recent = false
resource_type = "droplet"
name_regex = "^snap"
const testAccCheckDataSourceDigitalOceanSnapshot_regex = `
resource "digitalocean_volume" "foo" {
region = "nyc1"
name = "volume-%d"
size = 100
description = "peace makes plenty"
}
`
const testAccCheckDigitalOceanVolumeSnapshotDataSourceConfig = `
data "digitalocean_snapshot" "volume_test_snap" {
most_recent = true
resource_type = "volume"
name_regex = "^vol-snap"
resource "digitalocean_snapshot" "foo" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.foo.id}"
}
`
const testAccCheckDigitalOceanVolumeSnapshotDataSourceConfig_fail = `
data "digitalocean_snapshot" "volume_test_snap" {
most_recent = false
resource_type = "volume"
name_regex = "^vol-snap"
data "digitalocean_snapshot" "foobar" {
most_recent = true
name_regex = "^${digitalocean_snapshot.foo.name}"
}`
const testAccCheckDataSourceDigitalOceanSnapshot_region = `
resource "digitalocean_volume" "foo" {
region = "nyc1"
name = "volume-nyc-%d"
size = 100
description = "peace makes plenty"
}
`
resource "digitalocean_volume" "bar" {
region = "lon1"
name = "volume-lon-%d"
size = 100
description = "peace makes plenty"
}
resource "digitalocean_snapshot" "foo" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.foo.id}"
}
resource "digitalocean_snapshot" "bar" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.bar.id}"
}
data "digitalocean_snapshot" "foobar" {
name = "${digitalocean_snapshot.bar.name}"
region = "lon1"
}`

View File

@ -0,0 +1,32 @@
package digitalocean
import (
"testing"
"fmt"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccDigitalOceanSnapshot_importBasic(t *testing.T) {
resourceName := "digitalocean_snapshot.foobar"
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanSnapshotDestroy,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccCheckDigitalOceanSnapshotConfig_basic, rInt, rInt),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View File

@ -39,6 +39,7 @@ func Provider() terraform.ResourceProvider {
"digitalocean_floating_ip": resourceDigitalOceanFloatingIp(),
"digitalocean_loadbalancer": resourceDigitalOceanLoadbalancer(),
"digitalocean_record": resourceDigitalOceanRecord(),
"digitalocean_snapshot": resourceDigitalOceanSnapshot(),
"digitalocean_ssh_key": resourceDigitalOceanSSHKey(),
"digitalocean_tag": resourceDigitalOceanTag(),
"digitalocean_volume": resourceDigitalOceanVolume(),

View File

@ -0,0 +1,117 @@
package digitalocean
import (
"context"
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)
func resourceDigitalOceanSnapshot() *schema.Resource {
return &schema.Resource{
Create: resourceDigitalOceanSnapshotCreate,
Read: resourceDigitalOceanSnapshotRead,
Delete: resourceDigitalOceanSnapshotDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
},
"volume_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
},
"regions": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"size": {
Type: schema.TypeFloat,
Computed: true,
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"min_disk_size": {
Type: schema.TypeInt,
Computed: true,
},
},
}
}
func resourceDigitalOceanSnapshotCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
opts := &godo.SnapshotCreateRequest{
Name: d.Get("name").(string),
VolumeID: d.Get("volume_id").(string),
}
log.Printf("[DEBUG] Snapshot create configuration: %#v", opts)
snapshot, _, err := client.Storage.CreateSnapshot(context.Background(), opts)
if err != nil {
return fmt.Errorf("Error creating Snapshot: %s", err)
}
d.SetId(snapshot.ID)
log.Printf("[INFO] Snapshot name: %s", snapshot.Name)
return resourceDigitalOceanSnapshotRead(d, meta)
}
func resourceDigitalOceanSnapshotRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
snapshot, resp, err := client.Snapshots.Get(context.Background(), d.Id())
if err != nil {
// If the snapshot is somehow already destroyed, mark as
// successfully gone
if resp.StatusCode == 404 {
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving snapshot: %s", err)
}
d.Set("name", snapshot.Name)
d.Set("volume_id", snapshot.ResourceID)
d.Set("regions", snapshot.Regions)
d.Set("size", snapshot.SizeGigaBytes)
d.Set("created_at", snapshot.Created)
d.Set("min_disk_size", snapshot.MinDiskSize)
return nil
}
func resourceDigitalOceanSnapshotDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*godo.Client)
log.Printf("[INFO] Deleting snaphot: %s", d.Id())
_, err := client.Snapshots.Delete(context.Background(), d.Id())
if err != nil {
return fmt.Errorf("Error deleting snapshot: %s", err)
}
d.SetId("")
return nil
}

View File

@ -0,0 +1,139 @@
package digitalocean
import (
"context"
"fmt"
"log"
"testing"
"strings"
"github.com/digitalocean/godo"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func init() {
resource.AddTestSweepers("digitalocean_snapshot", &resource.Sweeper{
Name: "digitalocean_snapshot",
F: testSweepSnapshots,
Dependencies: []string{"digitalocean_volume"},
})
}
func testSweepSnapshots(region string) error {
meta, err := sharedConfigForRegion(region)
if err != nil {
return err
}
client := meta.(*godo.Client)
snapshots, _, err := client.Snapshots.ListVolume(context.Background(), nil)
if err != nil {
return err
}
for _, s := range snapshots {
if strings.HasPrefix(s.Name, "snapshot-") {
log.Printf("Destroying Snapshot %s", s.Name)
if _, err := client.Snapshots.Delete(context.Background(), s.ID); err != nil {
return err
}
}
}
return nil
}
func TestAccDigitalOceanSnapshot_Basic(t *testing.T) {
var snapshot godo.Snapshot
rInt := acctest.RandInt()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanSnapshotDestroy,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(testAccCheckDigitalOceanSnapshotConfig_basic, rInt, rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanSnapshotExists("digitalocean_snapshot.foobar", &snapshot),
resource.TestCheckResourceAttr(
"digitalocean_snapshot.foobar", "name", fmt.Sprintf("snapshot-%d", rInt)),
resource.TestCheckResourceAttr(
"digitalocean_snapshot.foobar", "size", "0"),
resource.TestCheckResourceAttr(
"digitalocean_snapshot.foobar", "regions.#", "1"),
resource.TestCheckResourceAttr(
"digitalocean_snapshot.foobar", "min_disk_size", "100"),
resource.TestCheckResourceAttrSet(
"digitalocean_snapshot.foobar", "volume_id"),
),
},
},
})
}
func testAccCheckDigitalOceanSnapshotExists(n string, snapshot *godo.Snapshot) resource.TestCheckFunc {
return func(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Snapshot ID is set")
}
foundSnapshot, _, err := client.Snapshots.Get(context.Background(), rs.Primary.ID)
if err != nil {
return err
}
if foundSnapshot.ID != rs.Primary.ID {
return fmt.Errorf("Snapshot not found")
}
*snapshot = *foundSnapshot
return nil
}
}
func testAccCheckDigitalOceanSnapshotDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*godo.Client)
for _, rs := range s.RootModule().Resources {
if rs.Type != "digitalocean_snapshot" {
continue
}
// Try to find the snapshot
_, _, err := client.Snapshots.Get(context.Background(), rs.Primary.ID)
if err == nil {
return fmt.Errorf("Snapshot still exists")
}
}
return nil
}
const testAccCheckDigitalOceanSnapshotConfig_basic = `
resource "digitalocean_volume" "foo" {
region = "nyc1"
name = "volume-%d"
size = 100
description = "peace makes plenty"
}
resource "digitalocean_snapshot" "foobar" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.foo.id}"
}`

View File

@ -47,6 +47,13 @@ func resourceDigitalOceanVolume() *schema.Resource {
ValidateFunc: validation.NoZeroValues,
},
"snapshot_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.NoZeroValues,
},
"initial_filesystem_type": {
Type: schema.TypeString,
Optional: true,
@ -86,12 +93,6 @@ func resourceDigitalOceanVolume() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"snapshot_id": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
CustomizeDiff: func(diff *schema.ResourceDiff, v interface{}) error {
@ -99,6 +100,7 @@ func resourceDigitalOceanVolume() *schema.Resource {
// if the new size of the volume is smaller than the old one return an error since
// only expanding the volume is allowed
oldSize, newSize := diff.GetChange("size")
log.Printf("QQQQQ %v %v", oldSize, newSize)
if newSize.(int) < oldSize.(int) {
return fmt.Errorf("volumes `size` can only be expanded and not shrunk")
}
@ -112,13 +114,19 @@ func resourceDigitalOceanVolumeCreate(d *schema.ResourceData, meta interface{})
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)),
SnapshotID: d.Get("snapshot_id").(string),
Name: d.Get("name").(string),
Description: d.Get("description").(string),
}
if v, ok := d.GetOk("region"); ok {
opts.Region = v.(string)
}
if v, ok := d.GetOk("size"); ok {
opts.SizeGigaBytes = int64(v.(int))
}
if v, ok := d.GetOk("snapshot_id"); ok {
opts.SnapshotID = v.(string)
}
if v, ok := d.GetOk("initial_filesystem_type"); ok {
opts.FilesystemType = v.(string)
} else if v, ok := d.GetOk("filesystem_type"); ok {
@ -181,6 +189,7 @@ func resourceDigitalOceanVolumeRead(d *schema.ResourceData, meta interface{}) er
return fmt.Errorf("Error retrieving volume: %s", err)
}
d.Set("region", volume.Region.Slug)
d.Set("size", int(volume.SizeGigaBytes))
if v := volume.FilesystemType; v != "" {
@ -238,6 +247,23 @@ func resourceDigitalOceanVolumeImport(rs *schema.ResourceData, v interface{}) ([
return []*schema.ResourceData{rs}, nil
}
// Seperate validation function to support common cumputed
func validateDigitalOceanVolumeSchema(d *schema.ResourceData) error {
_, hasRegion := d.GetOk("region")
_, hasSize := d.GetOk("size")
_, hasSnapshotId := d.GetOk("snapshot_id")
if !hasSnapshotId {
if !hasRegion {
return fmt.Errorf("`region` must be assigned when not specifying a `snapshot_id`")
}
if !hasSize {
return fmt.Errorf("`size` must be assigned when not specifying a `snapshot_id`")
}
}
return nil
}
func flattenDigitalOceanVolumeDropletIds(droplets []int) *schema.Set {
flattenedDroplets := schema.NewSet(schema.HashInt, []interface{}{})
for _, v := range droplets {

View File

@ -320,3 +320,50 @@ resource "digitalocean_droplet" "foobar" {
volume_ids = ["${digitalocean_volume.foobar.id}"]
}`, vName, vSize, rInt)
}
func TestAccDigitalOceanVolume_CreateFromSnapshot(t *testing.T) {
rInt := acctest.RandInt()
volume := godo.Volume{
Name: fmt.Sprintf("volume-snap-%d", rInt),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckDigitalOceanVolumeDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckDigitalOceanVolumeConfig_create_from_snapshot(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckDigitalOceanVolumeExists("digitalocean_volume.foobar", &volume),
// the droplet should see an attached volume
resource.TestCheckResourceAttr("digitalocean_volume.foobar", "region", "nyc1"),
resource.TestCheckResourceAttr("digitalocean_volume.foobar", "size", "100"),
),
},
},
})
}
func testAccCheckDigitalOceanVolumeConfig_create_from_snapshot(rInt int) string {
return fmt.Sprintf(`
resource "digitalocean_volume" "foo" {
region = "nyc1"
name = "volume-%d"
size = 100
description = "peace makes plenty"
}
resource "digitalocean_snapshot" "foo" {
name = "snapshot-%d"
volume_id = "${digitalocean_volume.foo.id}"
}
resource "digitalocean_volume" "foobar" {
region = "nyc1"
name = "volume-snap-%d"
size = "${digitalocean_snapshot.foo.min_disk_size}"
snapshot_id = "${digitalocean_snapshot.foo.id}"
}`, rInt, rInt, rInt)
}

View File

@ -8,29 +8,27 @@ description: |-
# digitalocean\_snapshot
Snapshots are saved instances of either a Droplet or a block storage volume. Use this data source to retrieve the ID of a DigitalOcean snapshot for use in other resources.
Snapshots are saved instances of a block storage volume. Use this data source to retrieve the ID of a DigitalOcean snapshot for use in other resources.
## Example Usage
```
data "digitalocean_snapshot" "snapshot" {
most_recent = true
name_regex = "^web"
region_filter = "nyc2"
resource_type = "droplet"
region= "nyc2"
most_recent = true
}
```
## Argument Reference
* `resource_type` - (Required) The type of DigitalOcean resource from which the snapshot originated. This currently must be either `droplet` or `volume`.
* `most_recent` - (Optional) If more than one result is returned, use the most
recent snapshot.
* `name` - (Optional) The name of the volume snapshot.
* `name_regex` - (Optional) A regex string to apply to the snapshot list returned by DigitalOcean. This allows more advanced filtering not supported from the DigitalOcean API. This filtering is done locally on what DigitalOcean returns.
* `region_filter` - (Optional) A "slug" representing a DigitalOcean region (e.g. `nyc1`). If set, only snapshots available in the region will be returned.
* `region` - (Optional) A "slug" representing a DigitalOcean region (e.g. `nyc1`). If set, only snapshots available in the region will be returned.
* `most_recent` - (Optional) If more than one result is returned, use the most recent snapshot.
~> **NOTE:** If more or less than a single match is returned by the search,
Terraform will fail. Ensure that your search is specific enough to return
@ -38,11 +36,11 @@ a single snapshot ID only, or use `most_recent` to choose the most recent one.
## Attributes Reference
`id` is set to the ID of the found snapshot. In addition, the following attributes are exported:
The following attributes are exported:
* `created_at` - The date and time the image was created.
* `min_disk_size` - The minimum size in gigabytes required for a volume or Droplet to be created based on this snapshot.
* `name` - The name of the snapshot.
* `id` The ID of the snapshot.
* `created_at` - The date and time the snapshot was created.
* `min_disk_size` - The minimum size in gigabytes required for a volume to be created based on this snapshot.
* `regions` - A list of DigitalOcean region "slugs" indicating where the snapshot is available.
* `resource_id` - The ID of the resource from which the snapshot originated.
* `size_gigabytes` - The billable size of the snapshot in gigabytes.
* `volume_id` - The ID of the volume from which the snapshot originated.
* `size` - The billable size of the snapshot in gigabytes.

View File

@ -0,0 +1,53 @@
---
layout: "digitalocean"
page_title: "DigitalOcean: digitalocean_snapshot"
sidebar_current: "docs-do-resource-snapshot"
description: |-
Provides a DigitalOcean snapshot resource.
---
# digitalocean\_volume
Provides a DigitalOcean Volume Snapshot which can be used to create a snapshot from an existing volume.
## Example Usage
```hcl
resource "digitalocean_volume" "foobar" {
region = "nyc1"
name = "baz"
size = 100
description = "an example volume"
}
resource "digitalocean_snapshot" "foobar" {
name = "foo"
volume_id = "${digitalocean_volume.foobar.id}"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) A name for the volume snapshot.
* `volume_id` - (Required) The ID of the volume from which the snapshot originated.
## Attributes Reference
The following attributes are exported:
* `id` The ID of the volume snapshot.
* `created_at` - The date and time the snapshot was created.
* `min_disk_size` - The minimum size in gigabytes required for a volume to be created based on this snapshot.
* `regions` - A list of DigitalOcean region "slugs" indicating where the snapshot is available.
* `size` - The billable size of the snapshot in gigabytes.
## Import
Snapshots can be imported using the `snapshot id`, e.g.
```
terraform import digitalocean_snapshot.snapshot 506f78a4-e098-11e5-ad9f-000f53306ae1
```

View File

@ -33,6 +33,21 @@ resource "digitalocean_volume_attachment" "foobar" {
}
```
You can also create a volume from an existing snapshot.
```hcl
data "digitalocean_snapshot" "foobar" {
name = "baz"
}
resource "digitalocean_volume" "foobar" {
region = "lon1"
name = "foo"
size = "${data.digitalocean_snapshot.foobar.min_disk_size}"
snapshot_id = "${data.digitalocean_snapshot.foobar.id}"
}
```
## Argument Reference
The following arguments are supported: