Added basic droplet property validations and updated the 'user_data' property to be stored as a hash in the state

This commit is contained in:
Tilen Faganel 2018-08-30 07:58:24 +01:00
parent fc729120f9
commit 8a67ab90bb
No known key found for this signature in database
GPG Key ID: 3DDA5ABF228F8E7A
4 changed files with 113 additions and 42 deletions

11
digitalocean/hash.go Normal file
View File

@ -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[:])
}

View File

@ -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
}

View File

@ -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"

View File

@ -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