From 7e04cee958c10255146660eef865e02edb9963dd Mon Sep 17 00:00:00 2001 From: Dana Hoffman Date: Thu, 28 Jun 2018 16:09:23 -0700 Subject: [PATCH] add new compute_instance_from_template resource (#1652) This was done as its own resource as suggested in slack, since we don't have the option of making all fields Computed in google_compute_instance. There's precedent in the aws provider for this sort of thing (see ami_copy, ami_from_instance). When I started working on this I assumed I could do it in the compute_instance resource and so I went ahead and reordered the schema to make it easier to work with in the future. Now it's not quite relevant, but I left it in as its own commit that can be looked at separately from the other changes. Fixes #1582. --- google/field_helpers.go | 4 + google/provider.go | 1 + google/resource_compute_instance.go | 555 +++++++++--------- ...resource_compute_instance_from_template.go | 139 +++++ ...rce_compute_instance_from_template_test.go | 109 ++++ vendor/github.com/stoewer/go-strcase/LICENSE | 21 + .../github.com/stoewer/go-strcase/README.md | 30 + vendor/github.com/stoewer/go-strcase/camel.go | 37 ++ vendor/github.com/stoewer/go-strcase/doc.go | 8 + .../github.com/stoewer/go-strcase/helper.go | 44 ++ vendor/github.com/stoewer/go-strcase/kebab.go | 9 + vendor/github.com/stoewer/go-strcase/snake.go | 48 ++ vendor/vendor.json | 6 + ...mpute_instance_from_template.html.markdown | 88 +++ website/google.erb | 4 + 15 files changed, 833 insertions(+), 270 deletions(-) create mode 100644 google/resource_compute_instance_from_template.go create mode 100644 google/resource_compute_instance_from_template_test.go create mode 100644 vendor/github.com/stoewer/go-strcase/LICENSE create mode 100644 vendor/github.com/stoewer/go-strcase/README.md create mode 100644 vendor/github.com/stoewer/go-strcase/camel.go create mode 100644 vendor/github.com/stoewer/go-strcase/doc.go create mode 100644 vendor/github.com/stoewer/go-strcase/helper.go create mode 100644 vendor/github.com/stoewer/go-strcase/kebab.go create mode 100644 vendor/github.com/stoewer/go-strcase/snake.go create mode 100644 website/docs/r/compute_instance_from_template.html.markdown diff --git a/google/field_helpers.go b/google/field_helpers.go index 33b1e889..4307c0f0 100644 --- a/google/field_helpers.go +++ b/google/field_helpers.go @@ -62,6 +62,10 @@ func ParseInstanceGroupFieldValue(instanceGroup string, d TerraformResourceData, return parseZonalFieldValue("instanceGroups", instanceGroup, "project", "zone", d, config, false) } +func ParseInstanceTemplateFieldValue(instanceTemplate string, d TerraformResourceData, config *Config) (*GlobalFieldValue, error) { + return parseGlobalFieldValue("instanceTemplates", instanceTemplate, "project", d, config, false) +} + func ParseSecurityPolicyFieldValue(securityPolicy string, d TerraformResourceData, config *Config) (*GlobalFieldValue, error) { return parseGlobalFieldValue("securityPolicies", securityPolicy, "project", d, config, true) } diff --git a/google/provider.go b/google/provider.go index 01b53e53..4dcf62ad 100644 --- a/google/provider.go +++ b/google/provider.go @@ -120,6 +120,7 @@ func Provider() terraform.ResourceProvider { "google_compute_health_check": resourceComputeHealthCheck(), "google_compute_image": resourceComputeImage(), "google_compute_instance": resourceComputeInstance(), + "google_compute_instance_from_template": resourceComputeInstanceFromTemplate(), "google_compute_instance_group": resourceComputeInstanceGroup(), "google_compute_instance_group_manager": resourceComputeInstanceGroupManager(), "google_compute_instance_template": resourceComputeInstanceTemplate(), diff --git a/google/resource_compute_instance.go b/google/resource_compute_instance.go index 18373faf..3efd064f 100644 --- a/google/resource_compute_instance.go +++ b/google/resource_compute_instance.go @@ -122,126 +122,6 @@ func resourceComputeInstance() *schema.Resource { }, }, - "scratch_disk": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "interface": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "SCSI", - ValidateFunc: validation.StringInSlice([]string{"SCSI", "NVME"}, false), - }, - }, - }, - }, - - "disk": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - ForceNew: true, - Removed: "Use boot_disk, scratch_disk, and attached_disk instead", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - // TODO(mitchellh): one of image or disk is required - - "disk": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "image": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "type": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "scratch": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - }, - - "auto_delete": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - - "size": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - ForceNew: true, - }, - - "device_name": &schema.Schema{ - Type: schema.TypeString, - Optional: 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, - }, - }, - }, - }, - - "attached_disk": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "source": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DiffSuppressFunc: linkDiffSuppress, - }, - - "device_name": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - - "mode": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "READ_WRITE", - ValidateFunc: validation.StringInSlice([]string{"READ_WRITE", "READ_ONLY"}, false), - }, - - "disk_encryption_key_raw": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Sensitive: true, - }, - - "disk_encryption_key_sha256": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - "machine_type": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -253,48 +133,6 @@ func resourceComputeInstance() *schema.Resource { ForceNew: true, }, - "instance_id": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, - - "zone": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - }, - - "can_ip_forward": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - }, - - "description": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "metadata": &schema.Schema{ - Type: schema.TypeMap, - Optional: true, - Elem: schema.TypeString, - }, - - "metadata_startup_script": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "metadata_fingerprint": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, - "network_interface": &schema.Schema{ Type: schema.TypeList, Required: true, @@ -400,6 +238,187 @@ func resourceComputeInstance() *schema.Resource { }, }, + "allow_stopping_for_update": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + + "attached_disk": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: linkDiffSuppress, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "READ_WRITE", + ValidateFunc: validation.StringInSlice([]string{"READ_WRITE", "READ_ONLY"}, false), + }, + + "disk_encryption_key_raw": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + + "disk_encryption_key_sha256": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "can_ip_forward": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + + "create_timeout": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 4, + Deprecated: "Use timeouts block instead.", + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "deletion_protection": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "disk": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Removed: "Use boot_disk, scratch_disk, and attached_disk instead", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // TODO(mitchellh): one of image or disk is required + + "disk": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "scratch": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "auto_delete": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Optional: 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, + }, + }, + }, + }, + + "guest_accelerator": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "count": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: linkDiffSuppress, + }, + }, + }, + }, + + "labels": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: schema.TypeString, + }, + + "metadata_startup_script": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "min_cpu_platform": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "network": &schema.Schema{ Type: schema.TypeList, Optional: true, @@ -444,11 +463,6 @@ func resourceComputeInstance() *schema.Resource { ForceNew: true, }, - "self_link": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, - "scheduling": &schema.Schema{ Type: schema.TypeList, MaxItems: 1, @@ -478,6 +492,22 @@ func resourceComputeInstance() *schema.Resource { }, }, + "scratch_disk": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "interface": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "SCSI", + ValidateFunc: validation.StringInSlice([]string{"SCSI", "NVME"}, false), + }, + }, + }, + }, + "service_account": &schema.Schema{ Type: schema.TypeList, MaxItems: 1, @@ -505,38 +535,6 @@ func resourceComputeInstance() *schema.Resource { }, }, - "guest_accelerator": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "count": &schema.Schema{ - Type: schema.TypeInt, - Required: true, - ForceNew: true, - }, - "type": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - DiffSuppressFunc: linkDiffSuppress, - }, - }, - }, - }, - - "cpu_platform": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, - - "min_cpu_platform": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - }, - "tags": &schema.Schema{ Type: schema.TypeSet, Optional: true, @@ -544,27 +542,21 @@ func resourceComputeInstance() *schema.Resource { Set: schema.HashString, }, - "tags_fingerprint": &schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "cpu_platform": &schema.Schema{ Type: schema.TypeString, Computed: true, }, - "labels": &schema.Schema{ - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, - }, - - "allow_stopping_for_update": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - }, - - "deletion_protection": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, }, "label_fingerprint": &schema.Schema{ @@ -572,11 +564,19 @@ func resourceComputeInstance() *schema.Resource { Computed: true, }, - "create_timeout": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 4, - Deprecated: "Use timeouts block instead.", + "metadata_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "tags_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, }, }, CustomizeDiff: customdiff.All( @@ -620,50 +620,36 @@ func getDisk(diskUri string, d *schema.ResourceData, config *Config) (*compute.D return disk, err } -func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - - project, err := getProject(d, config) - if err != nil { - return err - } - - // Get the zone - z, err := getZone(d, config) - if err != nil { - return err - } - log.Printf("[DEBUG] Loading zone: %s", z) - zone, err := config.clientCompute.Zones.Get( - project, z).Do() - if err != nil { - return fmt.Errorf( - "Error loading zone '%s': %s", z, err) - } - +func expandComputeInstance(project string, zone *compute.Zone, d *schema.ResourceData, config *Config) (*computeBeta.Instance, error) { // Get the machine type - log.Printf("[DEBUG] Loading machine type: %s", d.Get("machine_type").(string)) - machineType, err := config.clientCompute.MachineTypes.Get( - project, zone.Name, d.Get("machine_type").(string)).Do() - if err != nil { - return fmt.Errorf( - "Error loading machine type: %s", - err) + var machineTypeUrl string + if mt, ok := d.GetOk("machine_type"); ok { + log.Printf("[DEBUG] Loading machine type: %s", mt.(string)) + machineType, err := config.clientCompute.MachineTypes.Get( + project, zone.Name, mt.(string)).Do() + if err != nil { + return nil, fmt.Errorf( + "Error loading machine type: %s", + err) + } + machineTypeUrl = machineType.SelfLink } // Build up the list of disks disks := []*computeBeta.AttachedDisk{} - bootDisk, err := expandBootDisk(d, config, zone, project) - if err != nil { - return err + if _, hasBootDisk := d.GetOk("boot_disk"); hasBootDisk { + bootDisk, err := expandBootDisk(d, config, zone, project) + if err != nil { + return nil, err + } + disks = append(disks, bootDisk) } - disks = append(disks, bootDisk) if _, hasScratchDisk := d.GetOk("scratch_disk"); hasScratchDisk { scratchDisks, err := expandScratchDisks(d, config, zone, project) if err != nil { - return err + return nil, err } disks = append(disks, scratchDisks...) } @@ -674,7 +660,7 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err diskConfig := d.Get(fmt.Sprintf("attached_disk.%d", i)).(map[string]interface{}) disk, err := expandAttachedDisk(diskConfig, d, config) if err != nil { - return err + return nil, err } disks = append(disks, disk) @@ -696,34 +682,27 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err } scheduling.ForceSendFields = []string{"AutomaticRestart", "Preemptible"} - // Read create timeout - // Until "create_timeout" is removed, use that timeout if set. - createTimeout := int(d.Timeout(schema.TimeoutCreate).Minutes()) - if v, ok := d.GetOk("create_timeout"); ok && v != 4 { - createTimeout = v.(int) - } - metadata, err := resourceInstanceMetadata(d) if err != nil { - return fmt.Errorf("Error creating metadata: %s", err) + return nil, fmt.Errorf("Error creating metadata: %s", err) } networkInterfaces, err := expandNetworkInterfaces(d, config) if err != nil { - return fmt.Errorf("Error creating network interfaces: %s", err) + return nil, fmt.Errorf("Error creating network interfaces: %s", err) } accels, err := expandInstanceGuestAccelerators(d, config) if err != nil { - return fmt.Errorf("Error creating guest accelerators: %s", err) + return nil, fmt.Errorf("Error creating guest accelerators: %s", err) } // Create the instance information - instance := &computeBeta.Instance{ + return &computeBeta.Instance{ CanIpForward: d.Get("can_ip_forward").(bool), Description: d.Get("description").(string), Disks: disks, - MachineType: machineType.SelfLink, + MachineType: machineTypeUrl, Metadata: metadata, Name: d.Get("name").(string), NetworkInterfaces: networkInterfaces, @@ -734,6 +713,40 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err MinCpuPlatform: d.Get("min_cpu_platform").(string), Scheduling: scheduling, DeletionProtection: d.Get("deletion_protection").(bool), + ForceSendFields: []string{"CanIpForward", "DeletionProtection"}, + }, nil +} + +func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Get the zone + z, err := getZone(d, config) + if err != nil { + return err + } + log.Printf("[DEBUG] Loading zone: %s", z) + zone, err := config.clientCompute.Zones.Get( + project, z).Do() + if err != nil { + return fmt.Errorf("Error loading zone '%s': %s", z, err) + } + + instance, err := expandComputeInstance(project, zone, d, config) + if err != nil { + return err + } + + // Read create timeout + // Until "create_timeout" is removed, use that timeout if set. + createTimeout := int(d.Timeout(schema.TimeoutCreate).Minutes()) + if v, ok := d.GetOk("create_timeout"); ok && v != 4 { + createTimeout = v.(int) } log.Printf("[INFO] Requesting instance creation") @@ -804,7 +817,9 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error if err != nil { return err } - d.Set("network_interface", networkInterfaces) + if err := d.Set("network_interface", networkInterfaces); err != nil { + return err + } // Fall back on internal ip if there is no external ip. This makes sense in the situation where // terraform is being used on a cloud instance and can therefore access the instances it creates @@ -831,8 +846,8 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error d.Set("tags", convertStringArrToInterface(instance.Tags.Items)) } - if len(instance.Labels) > 0 { - d.Set("labels", instance.Labels) + if err := d.Set("labels", instance.Labels); err != nil { + return err } if instance.LabelFingerprint != "" { diff --git a/google/resource_compute_instance_from_template.go b/google/resource_compute_instance_from_template.go new file mode 100644 index 00000000..7dc50f4b --- /dev/null +++ b/google/resource_compute_instance_from_template.go @@ -0,0 +1,139 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + strcase "github.com/stoewer/go-strcase" +) + +func resourceComputeInstanceFromTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceFromTemplateCreate, + Read: resourceComputeInstanceRead, + Update: resourceComputeInstanceUpdate, + Delete: resourceComputeInstanceDelete, + + // Import doesn't really make sense, because you could just import + // as a google_compute_instance. + + Timeouts: resourceComputeInstance().Timeouts, + + Schema: computeInstanceFromTemplateSchema(), + CustomizeDiff: resourceComputeInstance().CustomizeDiff, + } +} + +func computeInstanceFromTemplateSchema() map[string]*schema.Schema { + s := resourceComputeInstance().Schema + + for _, field := range []string{"boot_disk", "machine_type", "network_interface"} { + s[field].Required = false + } + + // Remove deprecated/removed fields that are never d.Set. We can't + // programatically remove all of them, because some of them still have d.Set + // calls. + for _, field := range []string{"create_timeout", "disk", "network"} { + delete(s, field) + } + + recurseOnSchema(s, func(field *schema.Schema) { + // We don't want to accidentally use default values to override the instance + // template, so remove defaults. + field.Default = nil + + // Make non-required fields computed since they'll be set by the template. + // Leave deprecated and removed fields alone because we don't set them. + if !field.Required && !(field.Deprecated != "" || field.Removed != "") { + field.Computed = true + } + }) + + s["source_instance_template"] = &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + } + + return s +} + +func recurseOnSchema(s map[string]*schema.Schema, f func(*schema.Schema)) { + for _, field := range s { + f(field) + if e := field.Elem; e != nil { + if r, ok := e.(*schema.Resource); ok { + recurseOnSchema(r.Schema, f) + } + } + } +} + +func resourceComputeInstanceFromTemplateCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Get the zone + z, err := getZone(d, config) + if err != nil { + return err + } + log.Printf("[DEBUG] Loading zone: %s", z) + zone, err := config.clientCompute.Zones.Get(project, z).Do() + if err != nil { + return fmt.Errorf("Error loading zone '%s': %s", z, err) + } + + instance, err := expandComputeInstance(project, zone, d, config) + if err != nil { + return err + } + + // Force send all top-level fields in case they're overridden to zero values. + // TODO: consider doing so for nested fields as well. + for f, s := range computeInstanceFromTemplateSchema() { + // It seems that GetOkExists always returns true for sets. + // TODO: confirm this and file issue against Terraform core. + // In the meantime, don't force send sets. + if s.Type == schema.TypeSet { + continue + } + + if _, exists := d.GetOkExists(f); exists { + // Assume for now that all fields are exact snake_case versions of the API fields. + // This won't necessarily always be true, but it serves as a good approximation and + // can be adjusted later as we discover issues. + instance.ForceSendFields = append(instance.ForceSendFields, strcase.UpperCamelCase(f)) + } + } + + tpl, err := ParseInstanceTemplateFieldValue(d.Get("source_instance_template").(string), d, config) + if err != nil { + return err + } + + log.Printf("[INFO] Requesting instance creation") + op, err := config.clientComputeBeta.Instances.Insert(project, zone.Name, instance).SourceInstanceTemplate(tpl.RelativeLink()).Do() + if err != nil { + return fmt.Errorf("Error creating instance: %s", err) + } + + // Store the ID now + d.SetId(instance.Name) + + // Wait for the operation to complete + waitErr := computeSharedOperationWaitTime(config.clientCompute, op, project, int(d.Timeout(schema.TimeoutCreate).Minutes()), "instance to create") + if waitErr != nil { + // The resource didn't actually create + d.SetId("") + return waitErr + } + + return resourceComputeInstanceRead(d, meta) +} diff --git a/google/resource_compute_instance_from_template_test.go b/google/resource_compute_instance_from_template_test.go new file mode 100644 index 00000000..4332e0d0 --- /dev/null +++ b/google/resource_compute_instance_from_template_test.go @@ -0,0 +1,109 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + compute "google.golang.org/api/compute/v1" +) + +func TestAccComputeInstanceFromTemplate_basic(t *testing.T) { + t.Parallel() + + var instance compute.Instance + instanceName := fmt.Sprintf("terraform-test-%s", acctest.RandString(10)) + templateName := fmt.Sprintf("terraform-test-%s", acctest.RandString(10)) + resourceName := "google_compute_instance_from_template.foobar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceFromTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceFromTemplate_basic(instanceName, templateName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists(resourceName, &instance), + + // Check that fields were set based on the template + resource.TestCheckResourceAttr(resourceName, "machine_type", "n1-standard-1"), + resource.TestCheckResourceAttr(resourceName, "attached_disk.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckComputeInstanceFromTemplateDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_instance_from_template" { + continue + } + + _, err := config.clientCompute.Instances.Get( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Instance still exists") + } + } + + return nil +} + +func testAccComputeInstanceFromTemplate_basic(instance, template string) string { + return fmt.Sprintf(` +resource "google_compute_disk" "foobar" { + name = "%s" + image = "debian-8-jessie-v20160803" + size = 10 + type = "pd-ssd" + zone = "us-central1-a" +} + +resource "google_compute_instance_template" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + + disk { + source_image = "debian-cloud/debian-8" + auto_delete = true + disk_size_gb = 100 + boot = true + } + + disk { + source = "${google_compute_disk.foobar.name}" + auto_delete = false + boot = false + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + + can_ip_forward = true +} + +resource "google_compute_instance_from_template" "foobar" { + name = "%s" + zone = "us-central1-a" + + source_instance_template = "${google_compute_instance_template.foobar.self_link}" + + // Overrides + can_ip_forward = false + labels { + my_key = "my_value" + } +} +`, template, template, instance) +} diff --git a/vendor/github.com/stoewer/go-strcase/LICENSE b/vendor/github.com/stoewer/go-strcase/LICENSE new file mode 100644 index 00000000..a105a381 --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017, Adrian Stoewer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/stoewer/go-strcase/README.md b/vendor/github.com/stoewer/go-strcase/README.md new file mode 100644 index 00000000..6a690e2c --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/README.md @@ -0,0 +1,30 @@ +[![Build Status](https://travis-ci.org/stoewer/go-strcase.svg?branch=master)](https://travis-ci.org/stoewer/go-strcase) +[![Coverage Status](https://coveralls.io/repos/github/stoewer/go-strcase/badge.svg?branch=master)](https://coveralls.io/github/stoewer/go-strcase?branch=master) +[![GoDoc](https://godoc.org/github.com/stoewer/go-strcase?status.svg)](https://godoc.org/github.com/stoewer/go-strcase) +--- + +# Go strcase + +The package `strcase` converts between different kinds of naming formats such as camel case +(`CamelCase`), snake case (`snake_case`) or kebab case (`kebab-case`). +The package is designed to work only with strings consisting of standard ASCII letters. +Unicode is currently not supported. + +## Versioning and stability + +Although the master branch is supposed to remain always backward compatible, the repository +contains version tags in order to support vendoring tools such as `glide`. +The tag names follow semantic versioning conventions and have the following format `v1.0.0`. + + +## Install and use + +```sh +go get -u github.com/stoewer/go-strcase +``` + +```go +import "github.com/stoewer/go-strcase" + +var snake = strcase.SnakeCase("CamelCase") +``` diff --git a/vendor/github.com/stoewer/go-strcase/camel.go b/vendor/github.com/stoewer/go-strcase/camel.go new file mode 100644 index 00000000..12b7351c --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/camel.go @@ -0,0 +1,37 @@ +// Copyright (c) 2017, A. Stoewer +// All rights reserved. + +package strcase + +import ( + "strings" +) + +// UpperCamelCase converts a string into camel case starting with a upper case letter. +func UpperCamelCase(s string) string { + return camelCase(s, true) +} + +// LowerCamelCase converts a string into camel case starting with a lower case letter. +func LowerCamelCase(s string) string { + return camelCase(s, false) +} + +func camelCase(s string, upper bool) string { + s = strings.TrimSpace(s) + buffer := make([]rune, 0, len(s)) + + var prev rune + for _, curr := range s { + if !isDelimiter(curr) { + if isDelimiter(prev) || (upper && prev == 0) { + buffer = append(buffer, toUpper(curr)) + } else { + buffer = append(buffer, toLower(curr)) + } + } + prev = curr + } + + return string(buffer) +} diff --git a/vendor/github.com/stoewer/go-strcase/doc.go b/vendor/github.com/stoewer/go-strcase/doc.go new file mode 100644 index 00000000..66e25749 --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) 2017, A. Stoewer +// All rights reserved. + +// Package strcase converts between different kinds of naming formats such as camel case +// (CamelCase), snake case (snake_case) or kebab case (`kebab-case`). The package is designed +// to work only with strings consisting of standard ASCII letters. Unicode is currently not +// supported. +package strcase diff --git a/vendor/github.com/stoewer/go-strcase/helper.go b/vendor/github.com/stoewer/go-strcase/helper.go new file mode 100644 index 00000000..4d9ce712 --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/helper.go @@ -0,0 +1,44 @@ +// Copyright (c) 2017, A. Stoewer +// All rights reserved. + +package strcase + +// isLower checks if a character is lower case. More precisely it evaluates if it is +// in the range of ASCII character 'a' to 'z'. +func isLower(ch rune) bool { + return ch >= 'a' && ch <= 'z' +} + +// toLower converts a character in the range of ASCII characters 'A' to 'Z' to its lower +// case counterpart. Other characters remain the same. +func toLower(ch rune) rune { + if ch >= 'A' && ch <= 'Z' { + return ch + 32 + } + return ch +} + +// isLower checks if a character is upper case. More precisely it evaluates if it is +// in the range of ASCII characters 'A' to 'Z'. +func isUpper(ch rune) bool { + return ch >= 'A' && ch <= 'Z' +} + +// toLower converts a character in the range of ASCII characters 'a' to 'z' to its lower +// case counterpart. Other characters remain the same. +func toUpper(ch rune) rune { + if ch >= 'a' && ch <= 'z' { + return ch - 32 + } + return ch +} + +// isSpace checks if a character is some kind of whitespace. +func isSpace(ch rune) bool { + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' +} + +// isDelimiter checks if a character is some kind of whitespace or '_' or '-'. +func isDelimiter(ch rune) bool { + return ch == '-' || ch == '_' || isSpace(ch) +} diff --git a/vendor/github.com/stoewer/go-strcase/kebab.go b/vendor/github.com/stoewer/go-strcase/kebab.go new file mode 100644 index 00000000..87467fd0 --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/kebab.go @@ -0,0 +1,9 @@ +// Copyright (c) 2017, A. Stoewer +// All rights reserved. + +package strcase + +// KebabCase converts a string into kebab case. +func KebabCase(s string) string { + return lowerDelimiterCase(s, '-') +} diff --git a/vendor/github.com/stoewer/go-strcase/snake.go b/vendor/github.com/stoewer/go-strcase/snake.go new file mode 100644 index 00000000..417f181a --- /dev/null +++ b/vendor/github.com/stoewer/go-strcase/snake.go @@ -0,0 +1,48 @@ +// Copyright (c) 2017, A. Stoewer +// All rights reserved. + +package strcase + +import ( + "strings" +) + +// SnakeCase converts a string into snake case. +func SnakeCase(s string) string { + return lowerDelimiterCase(s, '_') +} + +// lowerDelimiterCase converts a string into snake_case or kebab-case depending on +// the delimiter passed in as second argument. +func lowerDelimiterCase(s string, delimiter rune) string { + s = strings.TrimSpace(s) + buffer := make([]rune, 0, len(s)+3) + + var prev rune + var curr rune + for _, next := range s { + if isDelimiter(curr) { + if !isDelimiter(prev) { + buffer = append(buffer, delimiter) + } + } else if isUpper(curr) { + if isLower(prev) || (isUpper(prev) && isLower(next)) { + buffer = append(buffer, delimiter) + } + buffer = append(buffer, toLower(curr)) + } else if curr != 0 { + buffer = append(buffer, curr) + } + prev = curr + curr = next + } + + if len(s) > 0 { + if isUpper(curr) && isLower(prev) && prev != 0 { + buffer = append(buffer, delimiter) + } + buffer = append(buffer, toLower(curr)) + } + + return string(buffer) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index eaa2f770..1ed6edd2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -974,6 +974,12 @@ "revision": "b061729afc07e77a8aa4fad0a2fd840958f1942a", "revisionTime": "2016-09-27T10:08:44Z" }, + { + "checksumSHA1": "t/Hcc8jNXkH58QfnotLNtpLh+qc=", + "path": "github.com/stoewer/go-strcase", + "revision": "c8136b55823dc6af966d084a06056c5575f6400f", + "revisionTime": "2017-04-24T18:08:47Z" + }, { "checksumSHA1": "qgMa75aMGbkFY0jIqqqgVnCUoNA=", "path": "github.com/ulikunitz/xz", diff --git a/website/docs/r/compute_instance_from_template.html.markdown b/website/docs/r/compute_instance_from_template.html.markdown new file mode 100644 index 00000000..85d08619 --- /dev/null +++ b/website/docs/r/compute_instance_from_template.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "google" +page_title: "Google: google_compute_instance_from_template" +sidebar_current: "docs-google-compute-instance-from-template" +description: |- + Manages a VM instance resource within GCE. +--- + +# google\_compute\_instance\_from\_template + +Manages a VM instance resource within GCE. For more information see +[the official documentation](https://cloud.google.com/compute/docs/instances) +and +[API](https://cloud.google.com/compute/docs/reference/latest/instances). + +This resource is specifically to create a compute instance from a given +`source_instance_template`. To create an instance without a template, use the +`google_compute_instance` resource. + + +## Example Usage + +```hcl +resource "google_compute_instance_template" "tpl" { + name = "template" + machine_type = "n1-standard-1" + + disk { + source_image = "debian-cloud/debian-8" + auto_delete = true + disk_size_gb = 100 + boot = true + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + + can_ip_forward = true +} + +resource "google_compute_instance_from_template" "tpl" { + name = "instance-from-template" + zone = "us-central1-a" + + source_instance_template = "${google_compute_instance_template.tpl.self_link}" + + // Override fields from instance template + can_ip_forward = false + labels { + my_key = "my_value" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `source_instance_template` - (Required) Name or self link of an instance + template to create the instance based on. + +- - - + +* `zone` - (Optional) The zone that the machine should be created in. If not + set, the provider zone is used. + +In addition to these, all arguments from `google_compute_instance` are supported +as a way to override the properties in the template. All exported attributes +from `google_compute_instance` are likewise exported here. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 6 minutes. +- `update` - Default is 6 minutes. +- `delete` - Default is 6 minutes. + diff --git a/website/google.erb b/website/google.erb index b8cd3039..e35a689b 100644 --- a/website/google.erb +++ b/website/google.erb @@ -299,6 +299,10 @@ google_compute_instance + > + google_compute_instance_from_template + + > google_compute_instance_group