From 549e1314f954a0d60e28ace6c5f9a07e1791ab9f Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Wed, 28 Jun 2017 15:36:00 -0700 Subject: [PATCH] Add `boot_disk` property to `google_compute_instance` (#122) * Add boot_disk property to google_compute_instance * docs for boot_disk * limit scope of bootDisk, use bool instead * test formatting * make device_name forcenew, add sha256 encryption key --- google/resource_compute_instance.go | 194 +++++++++++++++++- google/resource_compute_instance_test.go | 110 +++++++++- website/docs/r/compute_instance.html.markdown | 54 ++++- 3 files changed, 340 insertions(+), 18 deletions(-) diff --git a/google/resource_compute_instance.go b/google/resource_compute_instance.go index 146fb7f2..9176f8e6 100644 --- a/google/resource_compute_instance.go +++ b/google/resource_compute_instance.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" ) @@ -26,6 +27,86 @@ func resourceComputeInstance() *schema.Resource { MigrateState: resourceComputeInstanceMigrateState, Schema: map[string]*schema.Schema{ + "boot_disk": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "auto_delete": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "disk_encryption_key_raw": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Sensitive: true, + }, + + "disk_encryption_key_sha256": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "initialize_params": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + if v.(int) < 1 { + errors = append(errors, fmt.Errorf( + "%q must be greater than 0", k)) + } + return + }, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"pd-standard", "pd-ssd"}, false), + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + + "source": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"boot_disk.initialize_params"}, + }, + }, + }, + }, + "disk": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -407,12 +488,23 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err } // Build up the list of disks + + disks := []*compute.AttachedDisk{} + var hasBootDisk bool + if _, hasBootDisk = d.GetOk("boot_disk"); hasBootDisk { + bootDisk, err := expandBootDisk(d, config, zone, project) + if err != nil { + return err + } + disks = append(disks, bootDisk) + } + disksCount := d.Get("disk.#").(int) attachedDisksCount := d.Get("attached_disk.#").(int) - if disksCount+attachedDisksCount == 0 { - return fmt.Errorf("At least one disk or attached_disk must be set") + + if disksCount+attachedDisksCount == 0 && !hasBootDisk { + return fmt.Errorf("At least one disk, attached_disk, or boot_disk must be set") } - disks := make([]*compute.AttachedDisk, 0, disksCount+attachedDisksCount) for i := 0; i < disksCount; i++ { prefix := fmt.Sprintf("disk.%d", i) @@ -422,7 +514,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err var disk compute.AttachedDisk disk.Type = "PERSISTENT" disk.Mode = "READ_WRITE" - disk.Boot = i == 0 + disk.Boot = i == 0 && !hasBootDisk disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool) if _, ok := d.GetOk(prefix + ".disk"); ok { @@ -513,7 +605,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err AutoDelete: false, // Don't allow autodelete; let terraform handle disk deletion } - disk.Boot = i == 0 && disksCount == 0 // TODO(danawillow): This is super hacky, let's just add a boot field. + disk.Boot = i == 0 && disksCount == 0 && !hasBootDisk if v, ok := d.GetOk(prefix + ".device_name"); ok { disk.DeviceName = v.(string) @@ -868,9 +960,10 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error disksCount := d.Get("disk.#").(int) attachedDisksCount := d.Get("attached_disk.#").(int) - disks := make([]map[string]interface{}, 0, disksCount) - attachedDisks := make([]map[string]interface{}, 0, attachedDisksCount) + if _, ok := d.GetOk("boot_disk"); ok { + disksCount++ + } if expectedDisks := disksCount + attachedDisksCount; len(instance.Disks) != expectedDisks { return fmt.Errorf("Expected %d disks, API returned %d", expectedDisks, len(instance.Disks)) } @@ -882,8 +975,14 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error dIndex := 0 adIndex := 0 + disks := make([]map[string]interface{}, 0, disksCount) + attachedDisks := make([]map[string]interface{}, 0, attachedDisksCount) for _, disk := range instance.Disks { - if _, ok := attachedDiskSources[disk.Source]; !ok { + if _, ok := d.GetOk("boot_disk"); ok && disk.Boot { + // This disk is a boot disk and there is a boot disk set in the config, therefore + // this is the boot disk set in the config. + d.Set("boot_disk", flattenBootDisk(d, disk)) + } else if _, ok := attachedDiskSources[disk.Source]; !ok { di := map[string]interface{}{ "disk": d.Get(fmt.Sprintf("disk.%d.disk", dIndex)), "image": d.Get(fmt.Sprintf("disk.%d.image", dIndex)), @@ -1193,3 +1292,82 @@ func resourceInstanceTags(d *schema.ResourceData) *compute.Tags { return tags } + +func expandBootDisk(d *schema.ResourceData, config *Config, zone *compute.Zone, project string) (*compute.AttachedDisk, error) { + disk := &compute.AttachedDisk{ + AutoDelete: d.Get("boot_disk.0.auto_delete").(bool), + Boot: true, + } + + if v, ok := d.GetOk("boot_disk.0.device_name"); ok { + disk.DeviceName = v.(string) + } + + if v, ok := d.GetOk("boot_disk.0.disk_encryption_key_raw"); ok { + disk.DiskEncryptionKey = &compute.CustomerEncryptionKey{ + RawKey: v.(string), + } + } + + if v, ok := d.GetOk("boot_disk.0.source"); ok { + diskName := v.(string) + diskData, err := config.clientCompute.Disks.Get( + project, zone.Name, diskName).Do() + if err != nil { + return nil, fmt.Errorf("Error loading disk '%s': %s", diskName, err) + } + disk.Source = diskData.SelfLink + } + + if _, ok := d.GetOk("boot_disk.0.initialize_params"); ok { + disk.InitializeParams = &compute.AttachedDiskInitializeParams{} + + if v, ok := d.GetOk("boot_disk.0.initialize_params.0.size"); ok { + disk.InitializeParams.DiskSizeGb = int64(v.(int)) + } + + if v, ok := d.GetOk("boot_disk.0.initialize_params.0.type"); ok { + diskTypeName := v.(string) + diskType, err := readDiskType(config, zone, diskTypeName) + if err != nil { + return nil, fmt.Errorf("Error loading disk type '%s': %s", diskTypeName, err) + } + disk.InitializeParams.DiskType = diskType.Name + } + + if v, ok := d.GetOk("boot_disk.0.initialize_params.0.image"); ok { + imageName := v.(string) + imageUrl, err := resolveImage(config, imageName) + if err != nil { + return nil, fmt.Errorf("Error resolving image name '%s': %s", imageName, err) + } + + disk.InitializeParams.SourceImage = imageUrl + } + } + + return disk, nil +} + +func flattenBootDisk(d *schema.ResourceData, disk *compute.AttachedDisk) []map[string]interface{} { + sourceUrl := strings.Split(disk.Source, "/") + result := map[string]interface{}{ + "auto_delete": disk.AutoDelete, + "device_name": disk.DeviceName, + "source": sourceUrl[len(sourceUrl)-1], + // disk_encryption_key_raw is not returned from the API, so don't store it in state. + // If necessary in the future, this can be copied from what the user originally specified. + } + if disk.DiskEncryptionKey != nil { + result["disk_encryption_key_sha256"] = disk.DiskEncryptionKey.Sha256 + } + if v, ok := d.GetOk("boot_disk.0.initialize_params.#"); ok { + result["initialize_params.#"] = v.(int) + // initialize_params is not returned from the API, so don't store its values in state. + // If necessary in the future, this can be copied from what the user originally specified. + // However, because Terraform automatically sets `boot_disk.0.initialize_params.#` to 0 if + // nothing is set in state for it, set it to whatever it was set to before to avoid a perpetual diff. + } + + return []map[string]interface{}{result} +} diff --git a/google/resource_compute_instance_test.go b/google/resource_compute_instance_test.go index 782038f9..c173379b 100644 --- a/google/resource_compute_instance_test.go +++ b/google/resource_compute_instance_test.go @@ -267,6 +267,49 @@ func TestAccComputeInstance_attachedDisk(t *testing.T) { }) } +func TestAccComputeInstance_bootDisk(t *testing.T) { + var instance compute.Instance + var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_bootDisk(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceBootDisk(&instance, instanceName), + ), + }, + }, + }) +} + +func TestAccComputeInstance_bootDisk_source(t *testing.T) { + var instance compute.Instance + var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + var diskName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_bootDisk_source(diskName, instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceBootDisk(&instance, diskName), + ), + }, + }, + }) +} + func TestAccComputeInstance_noDisk(t *testing.T) { var instanceName = fmt.Sprintf("instance-test-%s", acctest.RandString(10)) @@ -277,7 +320,7 @@ func TestAccComputeInstance_noDisk(t *testing.T) { Steps: []resource.TestStep{ resource.TestStep{ Config: testAccComputeInstance_noDisk(instanceName), - ExpectError: regexp.MustCompile("At least one disk or attached_disk must be set"), + ExpectError: regexp.MustCompile("At least one disk, attached_disk, or boot_disk must be set"), }, }, }) @@ -751,7 +794,7 @@ func testAccCheckComputeInstanceDisk(instance *compute.Instance, source string, } for _, disk := range instance.Disks { - if strings.LastIndex(disk.Source, "/"+source) == len(disk.Source)-len(source)-1 && disk.AutoDelete == delete && disk.Boot == boot { + if strings.HasSuffix(disk.Source, source) && disk.AutoDelete == delete && disk.Boot == boot { return nil } } @@ -760,6 +803,24 @@ func testAccCheckComputeInstanceDisk(instance *compute.Instance, source string, } } +func testAccCheckComputeInstanceBootDisk(instance *compute.Instance, source string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instance.Disks == nil { + return fmt.Errorf("no disks") + } + + for _, disk := range instance.Disks { + if disk.Boot == true { + if strings.HasSuffix(disk.Source, source) { + return nil + } + } + } + + return fmt.Errorf("Boot disk not found with source %q", source) + } +} + func testAccCheckComputeInstanceDiskEncryptionKey(n string, instance *compute.Instance) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1232,6 +1293,51 @@ resource "google_compute_instance" "foobar" { `, disk, instance) } +func testAccComputeInstance_bootDisk(instance string) string { + return fmt.Sprintf(` +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + boot_disk { + initialize_params { + image = "debian-8-jessie-v20160803" + } + disk_encryption_key_raw = "SGVsbG8gZnJvbSBHb29nbGUgQ2xvdWQgUGxhdGZvcm0=" + } + + network_interface { + network = "default" + } +} +`, instance) +} + +func testAccComputeInstance_bootDisk_source(disk, instance string) string { + return fmt.Sprintf(` +resource "google_compute_disk" "foobar" { + name = "%s" + zone = "us-central1-a" + image = "debian-8-jessie-v20160803" +} + +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + boot_disk { + source = "${google_compute_disk.foobar.name}" + } + + network_interface { + network = "default" + } +} +`, disk, instance) +} + func testAccComputeInstance_noDisk(instance string) string { return fmt.Sprintf(` resource "google_compute_instance" "foobar" { diff --git a/website/docs/r/compute_instance.html.markdown b/website/docs/r/compute_instance.html.markdown index 500d6ba5..f58150d8 100644 --- a/website/docs/r/compute_instance.html.markdown +++ b/website/docs/r/compute_instance.html.markdown @@ -58,8 +58,8 @@ resource "google_compute_instance" "default" { The following arguments are supported: -* `disk` - (Required) Disks to attach to the instance. This can be specified - multiple times for multiple disks. Structure is documented below. +* `boot_disk` - (Required) The boot disk for the instance. + Structure is documented below. * `machine_type` - (Required) The machine type to create. To create a custom machine type, value should be set as specified @@ -80,8 +80,16 @@ The following arguments are supported: packets with non-matching source or destination IPs. This defaults to false. +* `create_timeout` - (Optional) Configurable timeout in minutes for creating instances. Default is 4 minutes. + Changing this forces a new resource to be created. + * `description` - (Optional) A brief description of this resource. +* `disk` - (Optional) Disks to attach to the instance. This can be specified + multiple times for multiple disks. Structure is documented below. + +* `labels` - (Optional) A set of key/value label pairs to assign to the instance. + * `metadata` - (Optional) Metadata key/value pairs to make available from within the instance. @@ -102,16 +110,46 @@ The following arguments are supported: * `tags` - (Optional) A list of tags to attach to the instance. -* `labels` - (Optional) A set of key/value label pairs to assign to the instance. +--- -* `create_timeout` - (Optional) Configurable timeout in minutes for creating instances. Default is 4 minutes. - Changing this forces a new resource to be created. +* `network` - (DEPRECATED) Networks to attach to the instance. This + can be specified multiple times for multiple networks. Structure is + documented below. --- -* `network` - (DEPRECATED, Required) Networks to attach to the instance. This - can be specified multiple times for multiple networks. Structure is - documented below. +The `boot_disk` block supports: + +* `auto_delete` - (Optional) Whether the disk will be auto-deleted when the instance + is deleted. Defaults to true. + +* `device_name` - (Optional) Name with which attached disk will be accessible + under `/dev/disk/by-id/` + +* `disk_encryption_key_raw` - (Optional) A 256-bit [customer-supplied encryption key] + (https://cloud.google.com/compute/docs/disks/customer-supplied-encryption), + encoded in [RFC 4648 base64](https://tools.ietf.org/html/rfc4648#section-4) + to encrypt this disk. + +* `initialize_params` - (Optional) Parameters for a new disk that will be created + alongside the new instance. Either `initialize_params` or `source` must be set. + Structure is documented below. + +* `source` - (Optional) The name of the existing disk (such as those managed by + `google_compute_disk`) to attach. + +The `initialize_params` block supports: + +* `size` - (Optional) The size of the image in gigabytes. If not specified, it + will inherit the size of its base image. + +* `type` - (Optional) The GCE disk type. May be set to pd-standard or pd-ssd. + +* `image` - (Optional) The image from which to initialize this disk. This can be + one of: the image's `self_link`, `projects/{project}/global/images/{image}`, + `projects/{project}/global/images/family/{family}`, `global/images/{image}`, + `global/images/family/{family}`, `family/{family}`, `{project}/{family}`, + `{project}/{image}`, `{family}`, or `{image}`. The `disk` block supports: (Note that either disk or image is required, unless the type is "local-ssd", in which case scratch must be true).