From c2a1e688cb19aef1ac76d7f360876824ce4f8c46 Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Fri, 21 Apr 2017 15:25:34 +0200 Subject: [PATCH] Add DigitalOcean datasource digitalocean_image (#13787) Add a new data source for Digital Ocean that finds snapshots by name. Signed-off-by: Julien Pivotto --- .../datasource_digitaloceal_image.go | 93 +++++++++++++ .../datasource_digitaloceal_image_test.go | 122 ++++++++++++++++++ builtin/providers/digitalocean/provider.go | 4 + .../source/docs/providers/do/d/image.html.md | 58 +++++++++ 4 files changed, 277 insertions(+) create mode 100644 builtin/providers/digitalocean/datasource_digitaloceal_image.go create mode 100644 builtin/providers/digitalocean/datasource_digitaloceal_image_test.go create mode 100644 website/source/docs/providers/do/d/image.html.md diff --git a/builtin/providers/digitalocean/datasource_digitaloceal_image.go b/builtin/providers/digitalocean/datasource_digitaloceal_image.go new file mode 100644 index 0000000000..d4023daf8d --- /dev/null +++ b/builtin/providers/digitalocean/datasource_digitaloceal_image.go @@ -0,0 +1,93 @@ +package digitalocean + +import ( + "fmt" + "strconv" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceDigitalOceanImage() *schema.Resource { + return &schema.Resource{ + Read: dataSourceDigitalOceanImageRead, + Schema: map[string]*schema.Schema{ + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + Description: "name of the image", + }, + // computed attributes + "image": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "slug or id of the image", + }, + "min_disk_size": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Description: "minimum disk size required by the image", + }, + "private": &schema.Schema{ + Type: schema.TypeBool, + Computed: true, + Description: "Is the image private or non-private", + }, + "regions": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Description: "list of the regions that the image is available in", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "type of the image", + }, + }, + } +} + +func dataSourceDigitalOceanImageRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*godo.Client) + + opts := &godo.ListOptions{} + + images, _, err := client.Images.ListUser(opts) + if err != nil { + d.SetId("") + return err + } + image, err := findImageByName(images, d.Get("name").(string)) + + if err != nil { + return err + } + + d.SetId(image.Name) + d.Set("name", image.Name) + d.Set("image", strconv.Itoa(image.ID)) + d.Set("min_disk_size", image.MinDiskSize) + d.Set("private", !image.Public) + d.Set("regions", image.Regions) + d.Set("type", image.Type) + + return nil +} + +func findImageByName(images []godo.Image, name string) (*godo.Image, error) { + results := make([]godo.Image, 0) + for _, v := range images { + if v.Name == name { + results = append(results, v) + } + } + if len(results) == 1 { + return &results[0], nil + } + if len(results) == 0 { + return nil, fmt.Errorf("no user image found with name %s", name) + } + return nil, fmt.Errorf("too many user images found with name %s (found %d, expected 1)", name, len(results)) +} diff --git a/builtin/providers/digitalocean/datasource_digitaloceal_image_test.go b/builtin/providers/digitalocean/datasource_digitaloceal_image_test.go new file mode 100644 index 0000000000..ab77c75ae4 --- /dev/null +++ b/builtin/providers/digitalocean/datasource_digitaloceal_image_test.go @@ -0,0 +1,122 @@ +package digitalocean + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/digitalocean/godo" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDigitalOceanImage_Basic(t *testing.T) { + var droplet godo.Droplet + var snapshotsId []int + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDigitalOceanDropletDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckDigitalOceanDropletConfig_basic(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + takeSnapshotsOfDroplet(rInt, &droplet, &snapshotsId), + ), + }, + { + Config: testAccCheckDigitalOceanImageConfig_basic(rInt, 1), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.digitalocean_image.foobar", "name", fmt.Sprintf("snap-%d-1", rInt)), + resource.TestCheckResourceAttr( + "data.digitalocean_image.foobar", "min_disk_size", "20"), + resource.TestCheckResourceAttr( + "data.digitalocean_image.foobar", "private", "true"), + resource.TestCheckResourceAttr( + "data.digitalocean_image.foobar", "type", "snapshot"), + ), + }, + { + Config: testAccCheckDigitalOceanImageConfig_basic(rInt, 0), + ExpectError: regexp.MustCompile(`.*too many user images found with name snap-.*\ .found 2, expected 1.`), + }, + { + Config: testAccCheckDigitalOceanImageConfig_nonexisting(rInt), + Destroy: false, + ExpectError: regexp.MustCompile(`.*no user image found with name snap-.*-nonexisting`), + }, + { + Config: " ", + Check: resource.ComposeTestCheckFunc( + deleteSnapshots(&snapshotsId), + ), + }, + }, + }) +} + +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++ { + err := takeSnapshotOfDroplet(rInt, i%2, droplet) + if err != nil { + return err + } + } + retrieveDroplet, _, err := client.Droplets.Get((*droplet).ID) + if err != nil { + return err + } + *snapshotsId = retrieveDroplet.SnapshotIDs + return nil + } +} + +func takeSnapshotOfDroplet(rInt, sInt int, droplet *godo.Droplet) error { + client := testAccProvider.Meta().(*godo.Client) + action, _, err := client.DropletActions.Snapshot((*droplet).ID, fmt.Sprintf("snap-%d-%d", rInt, sInt)) + if err != nil { + return err + } + waitForAction(client, action) + return nil +} + +func deleteSnapshots(snapshotsId *[]int) resource.TestCheckFunc { + return func(s *terraform.State) error { + log.Printf("XXX Deleting snaps") + client := testAccProvider.Meta().(*godo.Client) + snapshots := *snapshotsId + for _, value := range snapshots { + log.Printf("XXX Deleting %d", value) + _, err := client.Images.Delete(value) + if err != nil { + return err + } + } + return nil + } +} + +func testAccCheckDigitalOceanImageConfig_basic(rInt, sInt int) string { + return fmt.Sprintf(` +data "digitalocean_image" "foobar" { + name = "snap-%d-%d" +} +`, rInt, sInt) +} + +func testAccCheckDigitalOceanImageConfig_nonexisting(rInt int) string { + return fmt.Sprintf(` +data "digitalocean_image" "foobar" { + name = "snap-%d-nonexisting" +} +`, rInt) +} diff --git a/builtin/providers/digitalocean/provider.go b/builtin/providers/digitalocean/provider.go index 5ab2cab439..e885e08238 100644 --- a/builtin/providers/digitalocean/provider.go +++ b/builtin/providers/digitalocean/provider.go @@ -17,6 +17,10 @@ func Provider() terraform.ResourceProvider { }, }, + DataSourcesMap: map[string]*schema.Resource{ + "digitalocean_image": dataSourceDigitalOceanImage(), + }, + ResourcesMap: map[string]*schema.Resource{ "digitalocean_domain": resourceDigitalOceanDomain(), "digitalocean_droplet": resourceDigitalOceanDroplet(), diff --git a/website/source/docs/providers/do/d/image.html.md b/website/source/docs/providers/do/d/image.html.md new file mode 100644 index 0000000000..ffed23f25a --- /dev/null +++ b/website/source/docs/providers/do/d/image.html.md @@ -0,0 +1,58 @@ +--- +layout: "digitalocean" +page_title: "DigitalOcean: digitalocean_image" +sidebar_current: "docs-do-datasource-image" +description: |- + Get information on an snapshot. +--- + +# digitalocean_image + +Get information on an snapshot images. The aim of this datasource is to enable +you to build droplets based on snapshot names. + +An error is triggered if zero or more than one result is returned by the query. + +## Example Usage + +Get the data about a snapshot: + +```hcl +data "digitalocean_image" "example1" { + name = "example-1.0.0" +} +``` + +Reuse the data about a snapshot to create a droplet: + +```hcl +data "digitalocean_image" "example1" { + name = "example-1.0.0" +} +resource "digitalocean_droplet" "example1" { + image = "${data.digitalocean_image.example1.image}" + name = "example-1" + region = "nyc2" + size = "512mb" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - The name of the image. + +## Attributes Reference + +The following attributes are exported: + +* `name` - See Argument Reference above. +* `image` - The id of the image. +* `min_disk_size`: The minimum 'disk' required for the image. +* `private` - Is image a public image or not. Public images represents + Linux distributions or Application, while non-public images represent + snapshots and backups and are only available within your account. +* `regions`: The regions that the image is available in. +* `size_gigabytes`: The size of the image in gigabytes. +* `type`: Type of the image. Can be "snapshot" or "backup".