From 8a67ab90bb4c2a7a96360197487acf4acdba1192 Mon Sep 17 00:00:00 2001 From: Tilen Faganel Date: Thu, 30 Aug 2018 07:58:24 +0100 Subject: [PATCH] Added basic droplet property validations and updated the 'user_data' property to be stored as a hash in the state --- digitalocean/hash.go | 11 ++ digitalocean/resource_digitalocean_droplet.go | 108 +++++++++++++----- .../resource_digitalocean_droplet_test.go | 34 ++++-- website/docs/r/droplet.html.markdown | 2 +- 4 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 digitalocean/hash.go diff --git a/digitalocean/hash.go b/digitalocean/hash.go new file mode 100644 index 00000000..c6573b49 --- /dev/null +++ b/digitalocean/hash.go @@ -0,0 +1,11 @@ +package digitalocean + +import ( + "crypto/sha1" + "encoding/hex" +) + +func HashString(s string) string { + hash := sha1.Sum([]byte(s)) + return hex.EncodeToString(hash[:]) +} diff --git a/digitalocean/resource_digitalocean_droplet.go b/digitalocean/resource_digitalocean_droplet.go index 2c9a299b..61ba8eca 100644 --- a/digitalocean/resource_digitalocean_droplet.go +++ b/digitalocean/resource_digitalocean_droplet.go @@ -12,6 +12,7 @@ import ( "github.com/digitalocean/godo" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceDigitalOceanDroplet() *schema.Resource { @@ -26,14 +27,16 @@ func resourceDigitalOceanDroplet() *schema.Resource { Schema: map[string]*schema.Schema{ "image": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, }, "name": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, }, "region": { @@ -44,6 +47,7 @@ func resourceDigitalOceanDroplet() *schema.Resource { // DO API V2 region slug is always lowercase return strings.ToLower(val.(string)) }, + ValidateFunc: validation.NoZeroValues, }, "size": { @@ -53,6 +57,7 @@ func resourceDigitalOceanDroplet() *schema.Resource { // DO API V2 size slug is always lowercase return strings.ToLower(val.(string)) }, + ValidateFunc: validation.NoZeroValues, }, "disk": { @@ -94,11 +99,13 @@ func resourceDigitalOceanDroplet() *schema.Resource { "backups": { Type: schema.TypeBool, Optional: true, + Default: false, }, "ipv6": { Type: schema.TypeBool, Optional: true, + Default: false, }, "ipv6_address": { @@ -117,6 +124,7 @@ func resourceDigitalOceanDroplet() *schema.Resource { "private_networking": { Type: schema.TypeBool, Optional: true, + Default: false, }, "ipv4_address": { @@ -130,15 +138,32 @@ func resourceDigitalOceanDroplet() *schema.Resource { }, "ssh_keys": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.NoZeroValues, + }, + Set: schema.HashString, }, "user_data": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + StateFunc: func(v interface{}) string { + switch v.(type) { + case string: + return HashString(v.(string)) + default: + return "" + } + }, + // In order to support older statefiles with fully saved user data + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return new != "" && old == d.Get("user_data") + }, }, "volume_ids": { @@ -146,10 +171,12 @@ func resourceDigitalOceanDroplet() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, }, + "monitoring": { Type: schema.TypeBool, Optional: true, ForceNew: true, + Default: false, }, "tags": tagsSchema(), @@ -160,17 +187,25 @@ func resourceDigitalOceanDroplet() *schema.Resource { func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*godo.Client) + image := d.Get("image").(string) + // Build up our creation options opts := &godo.DropletCreateRequest{ - Image: godo.DropletCreateImage{ - Slug: d.Get("image").(string), - }, + Image: godo.DropletCreateImage{}, Name: d.Get("name").(string), Region: d.Get("region").(string), Size: d.Get("size").(string), Tags: expandTags(d.Get("tags").(*schema.Set).List()), } + imageId, err := strconv.Atoi(image) + if err == nil { + // The image field is provided as an ID (number). + opts.Image.ID = imageId + } else { + opts.Image.Slug = image + } + if attr, ok := d.GetOk("backups"); ok { opts.Backups = attr.(bool) } @@ -208,23 +243,12 @@ func resourceDigitalOceanDropletCreate(d *schema.ResourceData, meta interface{}) } // Get configured ssh_keys - sshKeys := d.Get("ssh_keys.#").(int) - if sshKeys > 0 { - opts.SSHKeys = make([]godo.DropletCreateSSHKey, 0, sshKeys) - for i := 0; i < sshKeys; i++ { - key := fmt.Sprintf("ssh_keys.%d", i) - sshKeyRef := d.Get(key).(string) - - var sshKey godo.DropletCreateSSHKey - // sshKeyRef can be either an ID or a fingerprint - if id, err := strconv.Atoi(sshKeyRef); err == nil { - sshKey.ID = id - } else { - sshKey.Fingerprint = sshKeyRef - } - - opts.SSHKeys = append(opts.SSHKeys, sshKey) + if v, ok := d.GetOk("ssh_keys"); ok { + expandedSshKeys, err := expandSshKeys(v.(*schema.Set).List()) + if err != nil { + return err } + opts.SSHKeys = expandedSshKeys } log.Printf("[DEBUG] Droplet create configuration: %#v", opts) @@ -332,6 +356,10 @@ func resourceDigitalOceanDropletRead(d *schema.ResourceData, meta interface{}) e func resourceDigitalOceanDropletImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { // This is a non API attribute. So set to the default setting in the schema. d.Set("resize_disk", true) + d.Set("backups", false) + d.Set("ipv6", false) + d.Set("private_networking", false) + d.Set("monitoring", false) err := resourceDigitalOceanDropletRead(d, meta) if err != nil { @@ -374,9 +402,9 @@ func resourceDigitalOceanDropletUpdate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("invalid droplet id: %v", err) } - resizeDisk := d.Get("resize_disk").(bool) - if resizeDisk && d.HasChange("size") { + if d.HasChange("size") { newSize := d.Get("size") + resizeDisk := d.Get("resize_disk").(bool) _, _, err = client.DropletActions.PowerOff(context.Background(), id) if err != nil && !strings.Contains(err.Error(), "Droplet is already powered off") { @@ -700,3 +728,21 @@ func detachVolumeIDOnDroplet(d *schema.ResourceData, volumeID string, meta inter return nil } + +func expandSshKeys(sshKeys []interface{}) ([]godo.DropletCreateSSHKey, error) { + expandedSshKeys := make([]godo.DropletCreateSSHKey, len(sshKeys)) + for i, s := range sshKeys { + sshKey := s.(string) + + var expandedSshKey godo.DropletCreateSSHKey + if id, err := strconv.Atoi(sshKey); err == nil { + expandedSshKey.ID = id + } else { + expandedSshKey.Fingerprint = sshKey + } + + expandedSshKeys[i] = expandedSshKey + } + + return expandedSshKeys, nil +} diff --git a/digitalocean/resource_digitalocean_droplet_test.go b/digitalocean/resource_digitalocean_droplet_test.go index 86740952..6220b71b 100644 --- a/digitalocean/resource_digitalocean_droplet_test.go +++ b/digitalocean/resource_digitalocean_droplet_test.go @@ -77,7 +77,7 @@ func TestAccDigitalOceanDroplet_Basic(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "region", "nyc3"), resource.TestCheckResourceAttr( - "digitalocean_droplet.foobar", "user_data", "foobar"), + "digitalocean_droplet.foobar", "user_data", HashString("foobar")), resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "ipv4_address_private", ""), resource.TestCheckResourceAttr( @@ -91,8 +91,8 @@ func TestAccDigitalOceanDroplet_Basic(t *testing.T) { func TestAccDigitalOceanDroplet_WithID(t *testing.T) { var droplet godo.Droplet rInt := acctest.RandInt() - // TODO: not hardcode this as it will change over time - centosID := 22995941 + // TODO: not hardcode this as it will change over time. Fix after the image datasource is updated + centosID := 34487567 resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -136,7 +136,7 @@ func TestAccDigitalOceanDroplet_withSSH(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "region", "nyc3"), resource.TestCheckResourceAttr( - "digitalocean_droplet.foobar", "user_data", "foobar"), + "digitalocean_droplet.foobar", "user_data", HashString("foobar")), ), }, }, @@ -215,7 +215,7 @@ func TestAccDigitalOceanDroplet_ResizeWithOutDisk(t *testing.T) { }) } -func TestAccDigitalOceanDroplet_ResizeOnlyDisk(t *testing.T) { +func TestAccDigitalOceanDroplet_ResizeSmaller(t *testing.T) { var droplet godo.Droplet rInt := acctest.RandInt() @@ -249,10 +249,24 @@ func TestAccDigitalOceanDroplet_ResizeOnlyDisk(t *testing.T) { }, { - Config: testAccCheckDigitalOceanDropletConfig_resize_only_disk(rInt), + Config: testAccCheckDigitalOceanDropletConfig_basic(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), - testAccCheckDigitalOceanDropletResizeOnlyDisk(&droplet), + testAccCheckDigitalOceanDropletAttributes(&droplet), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "name", fmt.Sprintf("foo-%d", rInt)), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "size", "512mb"), + resource.TestCheckResourceAttr( + "digitalocean_droplet.foobar", "disk", "20"), + ), + }, + + { + Config: testAccCheckDigitalOceanDropletConfig_resize(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckDigitalOceanDropletExists("digitalocean_droplet.foobar", &droplet), + testAccCheckDigitalOceanDropletResizeSmaller(&droplet), resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "name", fmt.Sprintf("foo-%d", rInt)), resource.TestCheckResourceAttr( @@ -293,7 +307,7 @@ func TestAccDigitalOceanDroplet_UpdateUserData(t *testing.T) { resource.TestCheckResourceAttr( "digitalocean_droplet.foobar", "user_data", - "foobar foobar"), + HashString("foobar foobar")), testAccCheckDigitalOceanDropletRecreated( t, &afterCreate, &afterUpdate), ), @@ -535,7 +549,7 @@ func testAccCheckDigitalOceanDropletResizeWithOutDisk(droplet *godo.Droplet) res } } -func testAccCheckDigitalOceanDropletResizeOnlyDisk(droplet *godo.Droplet) resource.TestCheckFunc { +func testAccCheckDigitalOceanDropletResizeSmaller(droplet *godo.Droplet) resource.TestCheckFunc { return func(s *terraform.State) error { if droplet.Size.Slug != "1gb" { @@ -732,7 +746,7 @@ resource "digitalocean_droplet" "foobar" { `, rInt) } -func testAccCheckDigitalOceanDropletConfig_resize_only_disk(rInt int) string { +func testAccCheckDigitalOceanDropletConfig_resize(rInt int) string { return fmt.Sprintf(` resource "digitalocean_droplet" "foobar" { name = "foo-%d" diff --git a/website/docs/r/droplet.html.markdown b/website/docs/r/droplet.html.markdown index f55982dc..9e978440 100644 --- a/website/docs/r/droplet.html.markdown +++ b/website/docs/r/droplet.html.markdown @@ -29,7 +29,7 @@ resource "digitalocean_droplet" "web" { The following arguments are supported: * `image` - (Required) The Droplet image ID or slug. -* `name` - (Required) The Droplet name +* `name` - (Required) The Droplet name. * `region` - (Required) The region to start in * `size` - (Required) The unique slug that indentifies the type of Droplet. You can find a list of available slugs on [DigitalOcean API documentation](https://developers.digitalocean.com/documentation/v2/#list-all-sizes) * `backups` - (Optional) Boolean controlling if backups are made. Defaults to