diff --git a/digitalocean/datasource_digitalocean_image_test.go b/digitalocean/datasource_digitalocean_image_test.go index d0449733..b5d38ed8 100644 --- a/digitalocean/datasource_digitalocean_image_test.go +++ b/digitalocean/datasource_digitalocean_image_test.go @@ -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 diff --git a/digitalocean/datasource_digitalocean_snapshot.go b/digitalocean/datasource_digitalocean_snapshot.go index e68dc4c8..093aa51d 100644 --- a/digitalocean/datasource_digitalocean_snapshot.go +++ b/digitalocean/datasource_digitalocean_snapshot.go @@ -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] } diff --git a/digitalocean/datasource_digitalocean_snapshot_test.go b/digitalocean/datasource_digitalocean_snapshot_test.go index 7f45b6a9..0868ab1b 100644 --- a/digitalocean/datasource_digitalocean_snapshot_test.go +++ b/digitalocean/datasource_digitalocean_snapshot_test.go @@ -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" +}` diff --git a/digitalocean/import_digitalocean_snapshot_test.go b/digitalocean/import_digitalocean_snapshot_test.go new file mode 100644 index 00000000..28418265 --- /dev/null +++ b/digitalocean/import_digitalocean_snapshot_test.go @@ -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, + }, + }, + }) +} diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 911f59bf..bcf2f921 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -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(), diff --git a/digitalocean/resource_digitalocean_snapshot.go b/digitalocean/resource_digitalocean_snapshot.go new file mode 100644 index 00000000..3dc42b10 --- /dev/null +++ b/digitalocean/resource_digitalocean_snapshot.go @@ -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 +} diff --git a/digitalocean/resource_digitalocean_snapshot_test.go b/digitalocean/resource_digitalocean_snapshot_test.go new file mode 100644 index 00000000..379cdd91 --- /dev/null +++ b/digitalocean/resource_digitalocean_snapshot_test.go @@ -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}" +}` diff --git a/digitalocean/resource_digitalocean_volume.go b/digitalocean/resource_digitalocean_volume.go index 22ba43d6..5b3c28d3 100644 --- a/digitalocean/resource_digitalocean_volume.go +++ b/digitalocean/resource_digitalocean_volume.go @@ -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 { diff --git a/digitalocean/resource_digitalocean_volume_test.go b/digitalocean/resource_digitalocean_volume_test.go index d9644388..33b7b0e6 100644 --- a/digitalocean/resource_digitalocean_volume_test.go +++ b/digitalocean/resource_digitalocean_volume_test.go @@ -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) +} diff --git a/website/docs/d/snapshot.html.md b/website/docs/d/snapshot.html.md index 802561e3..ab11bc4a 100644 --- a/website/docs/d/snapshot.html.md +++ b/website/docs/d/snapshot.html.md @@ -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. \ No newline at end of file diff --git a/website/docs/r/snapshot.html.markdown b/website/docs/r/snapshot.html.markdown new file mode 100644 index 00000000..00c76e8d --- /dev/null +++ b/website/docs/r/snapshot.html.markdown @@ -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 +``` diff --git a/website/docs/r/volume.html.markdown b/website/docs/r/volume.html.markdown index eab7a1e2..301b7437 100644 --- a/website/docs/r/volume.html.markdown +++ b/website/docs/r/volume.html.markdown @@ -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: