diff --git a/google/resource_compute_instance.go b/google/resource_compute_instance.go index 08247b292b8..6759aecd3b4 100644 --- a/google/resource_compute_instance.go +++ b/google/resource_compute_instance.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform-plugin-sdk/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" "github.com/mitchellh/hashstructure" @@ -537,6 +538,12 @@ func resourceComputeInstance() *schema.Resource { }, }, + "desired_status": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"RUNNING", "TERMINATED"}, false), + }, + "tags": { Type: schema.TypeSet, Optional: true, @@ -594,6 +601,7 @@ func resourceComputeInstance() *schema.Resource { }, suppressEmptyGuestAcceleratorDiff, ), + desiredStatusDiff, ), } } @@ -715,6 +723,59 @@ func expandComputeInstance(project string, d *schema.ResourceData, config *Confi }, nil } +var computeInstanceStatus = []string{ + "PROVISIONING", + "REPAIRING", + "RUNNING", + "STAGING", + "STOPPED", + "STOPPING", + "SUSPENDED", + "SUSPENDING", + "TERMINATED", +} + +// return all possible Compute instances status except the one passed as parameter +func getAllStatusBut(status string) []string { + for i, s := range computeInstanceStatus { + if status == s { + return append(computeInstanceStatus[:i], computeInstanceStatus[i+1:]...) + } + } + return computeInstanceStatus +} + +func waitUntilInstanceHasDesiredStatus(config *Config, d *schema.ResourceData) error { + desiredStatus := d.Get("desired_status").(string) + + if desiredStatus != "" { + stateRefreshFunc := func() (interface{}, string, error) { + instance, err := getInstance(config, d) + if err != nil || instance == nil { + log.Printf("Error on InstanceStateRefresh: %s", err) + return nil, "", err + } + return instance.Id, instance.Status, nil + } + stateChangeConf := resource.StateChangeConf{ + Delay: 5 * time.Second, + Pending: getAllStatusBut(desiredStatus), + Refresh: stateRefreshFunc, + Target: []string{desiredStatus}, + Timeout: d.Timeout(schema.TimeoutUpdate), + MinTimeout: 2 * time.Second, + } + _, err := stateChangeConf.WaitForState() + + if err != nil { + return fmt.Errorf( + "Error waiting for instance to reach desired status %s: %s", desiredStatus, err) + } + } + + return nil +} + func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -760,6 +821,11 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err return waitErr } + err = waitUntilInstanceHasDesiredStatus(config, d) + if err != nil { + return fmt.Errorf("Error waiting for status: %s", err) + } + return resourceComputeInstanceRead(d, meta) } @@ -945,6 +1011,11 @@ func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error d.Set("name", instance.Name) d.Set("description", instance.Description) d.Set("hostname", instance.Hostname) + + if d.Get("desired_status") != "" { + d.Set("desired_status", instance.Status) + } + d.SetId(fmt.Sprintf("projects/%s/zones/%s/instances/%s", project, zone, instance.Name)) return nil @@ -1311,20 +1382,56 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err d.SetPartial("deletion_protection") } - // Attributes which can only be changed if the instance is stopped - if scopesChange || d.HasChange("service_account.0.email") || d.HasChange("machine_type") || d.HasChange("min_cpu_platform") || d.HasChange("enable_display") { - if !d.Get("allow_stopping_for_update").(bool) { - return fmt.Errorf("Changing the machine_type, min_cpu_platform, service_account, or enable display on an instance requires stopping it. " + - "To acknowledge this, please set allow_stopping_for_update = true in your config.") + needToStopInstanceBeforeUpdating := scopesChange || d.HasChange("service_account.0.email") || d.HasChange("machine_type") || d.HasChange("min_cpu_platform") || d.HasChange("enable_display") + + if d.HasChange("desired_status") && !needToStopInstanceBeforeUpdating { + desiredStatus := d.Get("desired_status").(string) + + if desiredStatus != "" { + var op *compute.Operation + + if desiredStatus == "RUNNING" { + op, err = startInstanceOperation(d, config) + if err != nil { + return errwrap.Wrapf("Error starting instance: {{err}}", err) + } + } else if desiredStatus == "TERMINATED" { + op, err = config.clientCompute.Instances.Stop(project, zone, instance.Name).Do() + if err != nil { + return err + } + } + opErr := computeOperationWaitTime( + config, op, project, "updating status", + int(d.Timeout(schema.TimeoutUpdate).Minutes())) + if opErr != nil { + return opErr + } } - op, err := config.clientCompute.Instances.Stop(project, zone, instance.Name).Do() - if err != nil { - return errwrap.Wrapf("Error stopping instance: {{err}}", err) + d.SetPartial("desired_status") + } + + // Attributes which can only be changed if the instance is stopped + if needToStopInstanceBeforeUpdating { + statusBeforeUpdate := instance.Status + desiredStatus := d.Get("desired_status").(string) + + if statusBeforeUpdate == "RUNNING" && desiredStatus != "TERMINATED" && !d.Get("allow_stopping_for_update").(bool) { + return fmt.Errorf("Changing the machine_type, min_cpu_platform, service_account, or enable display on a started instance requires stopping it. " + + "To acknowledge this, please set allow_stopping_for_update = true in your config. " + + "You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not be restarted after the update.") } - opErr := computeOperationWaitTime(config, op, project, "stopping instance", int(d.Timeout(schema.TimeoutUpdate).Minutes())) - if opErr != nil { - return opErr + if statusBeforeUpdate != "TERMINATED" { + op, err := config.clientCompute.Instances.Stop(project, zone, instance.Name).Do() + if err != nil { + return errwrap.Wrapf("Error stopping instance: {{err}}", err) + } + + opErr := computeOperationWaitTime(config, op, project, "stopping instance", int(d.Timeout(schema.TimeoutUpdate).Minutes())) + if opErr != nil { + return opErr + } } if d.HasChange("machine_type") { @@ -1335,7 +1442,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err req := &compute.InstancesSetMachineTypeRequest{ MachineType: mt.RelativeLink(), } - op, err = config.clientCompute.Instances.SetMachineType(project, zone, instance.Name, req).Do() + op, err := config.clientCompute.Instances.SetMachineType(project, zone, instance.Name, req).Do() if err != nil { return err } @@ -1357,7 +1464,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err req := &compute.InstancesSetMinCpuPlatformRequest{ MinCpuPlatform: minCpuPlatform.(string), } - op, err = config.clientCompute.Instances.SetMinCpuPlatform(project, zone, instance.Name, req).Do() + op, err := config.clientCompute.Instances.SetMinCpuPlatform(project, zone, instance.Name, req).Do() if err != nil { return err } @@ -1376,7 +1483,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err req.Email = saMap["email"].(string) req.Scopes = canonicalizeServiceScopes(convertStringSet(saMap["scopes"].(*schema.Set))) } - op, err = config.clientCompute.Instances.SetServiceAccount(project, zone, instance.Name, req).Do() + op, err := config.clientCompute.Instances.SetServiceAccount(project, zone, instance.Name, req).Do() if err != nil { return err } @@ -1392,7 +1499,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err EnableDisplay: d.Get("enable_display").(bool), ForceSendFields: []string{"EnableDisplay"}, } - op, err = config.clientCompute.Instances.UpdateDisplayDevice(project, zone, instance.Name, req).Do() + op, err := config.clientCompute.Instances.UpdateDisplayDevice(project, zone, instance.Name, req).Do() if err != nil { return fmt.Errorf("Error updating display device: %s", err) } @@ -1403,35 +1510,18 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err d.SetPartial("enable_display") } - // Retrieve instance from config to pull encryption keys if necessary - instanceFromConfig, err := expandComputeInstance(project, d, config) - if err != nil { - return err - } - - var encrypted []*compute.CustomerEncryptionKeyProtectedDisk - for _, disk := range instanceFromConfig.Disks { - if disk.DiskEncryptionKey != nil { - key := compute.CustomerEncryptionKey{RawKey: disk.DiskEncryptionKey.RawKey, KmsKeyName: disk.DiskEncryptionKey.KmsKeyName} - eDisk := compute.CustomerEncryptionKeyProtectedDisk{Source: disk.Source, DiskEncryptionKey: &key} - encrypted = append(encrypted, &eDisk) + if (statusBeforeUpdate == "RUNNING" && desiredStatus != "TERMINATED") || + (statusBeforeUpdate == "TERMINATED" && desiredStatus == "RUNNING") { + op, err := startInstanceOperation(d, config) + if err != nil { + return errwrap.Wrapf("Error starting instance: {{err}}", err) } - } - if len(encrypted) > 0 { - request := compute.InstancesStartWithEncryptionKeyRequest{Disks: encrypted} - op, err = config.clientCompute.Instances.StartWithEncryptionKey(project, zone, instance.Name, &request).Do() - } else { - op, err = config.clientCompute.Instances.Start(project, zone, instance.Name).Do() - } - if err != nil { - return errwrap.Wrapf("Error starting instance: {{err}}", err) - } - - opErr = computeOperationWaitTime(config, op, project, - "starting instance", int(d.Timeout(schema.TimeoutUpdate).Minutes())) - if opErr != nil { - return opErr + opErr := computeOperationWaitTime(config, op, project, + "starting instance", int(d.Timeout(schema.TimeoutUpdate).Minutes())) + if opErr != nil { + return opErr + } } } @@ -1458,6 +1548,51 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err return resourceComputeInstanceRead(d, meta) } +func startInstanceOperation(d *schema.ResourceData, config *Config) (*compute.Operation, error) { + project, err := getProject(d, config) + if err != nil { + return nil, err + } + + zone, err := getZone(d, config) + if err != nil { + return nil, err + } + + // Use beta api directly in order to read network_interface.fingerprint without having to put it in the schema. + // Change back to getInstance(config, d) once updating alias ips is GA. + instance, err := config.clientComputeBeta.Instances.Get(project, zone, d.Get("name").(string)).Do() + if err != nil { + return nil, handleNotFoundError(err, d, fmt.Sprintf("Instance %s", instance.Name)) + } + + // Retrieve instance from config to pull encryption keys if necessary + instanceFromConfig, err := expandComputeInstance(project, d, config) + if err != nil { + return nil, err + } + + var encrypted []*compute.CustomerEncryptionKeyProtectedDisk + for _, disk := range instanceFromConfig.Disks { + if disk.DiskEncryptionKey != nil { + key := compute.CustomerEncryptionKey{RawKey: disk.DiskEncryptionKey.RawKey, KmsKeyName: disk.DiskEncryptionKey.KmsKeyName} + eDisk := compute.CustomerEncryptionKeyProtectedDisk{Source: disk.Source, DiskEncryptionKey: &key} + encrypted = append(encrypted, &eDisk) + } + } + + var op *compute.Operation + + if len(encrypted) > 0 { + request := compute.InstancesStartWithEncryptionKeyRequest{Disks: encrypted} + op, err = config.clientCompute.Instances.StartWithEncryptionKey(project, zone, instance.Name, &request).Do() + } else { + op, err = config.clientCompute.Instances.Start(project, zone, instance.Name).Do() + } + + return op, err +} + func expandAttachedDisk(diskConfig map[string]interface{}, d *schema.ResourceData, meta interface{}) (*computeBeta.AttachedDisk, error) { config := meta.(*Config) @@ -1581,6 +1716,25 @@ func suppressEmptyGuestAcceleratorDiff(d *schema.ResourceDiff, meta interface{}) return nil } +// return an error if the desired_status field is set to a value other than RUNNING on Create. +func desiredStatusDiff(diff *schema.ResourceDiff, meta interface{}) error { + // when creating an instance, name is not set + oldName, _ := diff.GetChange("name") + + if oldName == nil || oldName == "" { + _, newDesiredStatus := diff.GetChange("desired_status") + + if newDesiredStatus == nil || newDesiredStatus == "" { + return nil + } else if newDesiredStatus != "RUNNING" { + return fmt.Errorf("When creating an instance, desired_status can only accept RUNNING value") + } + return nil + } + + return nil +} + func resourceComputeInstanceDelete(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) diff --git a/google/resource_compute_instance_test.go b/google/resource_compute_instance_test.go index 84556d13c5b..78fb37790d7 100644 --- a/google/resource_compute_instance_test.go +++ b/google/resource_compute_instance_test.go @@ -2,6 +2,7 @@ package google import ( "fmt" + "regexp" "strconv" "strings" "testing" @@ -1262,6 +1263,527 @@ func TestAccComputeInstance_enableDisplay(t *testing.T) { }) } +func TestAccComputeInstance_desiredStatusOnCreation(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + ExpectError: regexp.MustCompile("When creating an instance, desired_status can only accept RUNNING value"), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "RUNNING", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_desiredStatusUpdateBasic(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "RUNNING", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "RUNNING", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_desiredStatusTerminatedUpdateFields(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_desiredStatusTerminatedUpdate(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceMetadata( + &instance, "bar", "baz"), + testAccCheckComputeInstanceLabel(&instance, "only_me", "nothing_else"), + testAccCheckComputeInstanceTag(&instance, "baz"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateRunning_desiredStatusRunning_allowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "RUNNING", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateRunning_desiredStatusNotSet_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "", false), + ExpectError: regexp.MustCompile("Changing the machine_type, min_cpu_platform, service_account, " + + "or enable display on a started instance requires stopping it. To acknowledge this, please set " + + "allow_stopping_for_update = true in your config. " + + "You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not " + + "be restarted after the update."), + }, + }, + }) +} + +func TestAccComputeInstance_updateRunning_desiredStatusRunning_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "RUNNING", false), + ExpectError: regexp.MustCompile("Changing the machine_type, min_cpu_platform, service_account, " + + "or enable display on a started instance requires stopping it. To acknowledge this, please set " + + "allow_stopping_for_update = true in your config. " + + "You can also stop it by setting desired_status = \"TERMINATED\", but the instance will not " + + "be restarted after the update."), + }, + }, + }) +} + +func TestAccComputeInstance_updateRunning_desiredStatusTerminated_allowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "TERMINATED", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateRunning_desiredStatusTerminated_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusNotSet_allowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusTerminated_allowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "TERMINATED", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusNotSet_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusTerminated_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusRunning_allowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "RUNNING", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_updateTerminated_desiredStatusRunning_notAllowStoppingForUpdate(t *testing.T) { + t.Parallel() + + 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{ + { + Config: testAccComputeInstance_basic2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-1", "TERMINATED", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasStatus(&instance, "TERMINATED"), + ), + }, + { + Config: testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate(instanceName, "n1-standard-2", "RUNNING", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceHasMachineType(&instance, "n1-standard-2"), + testAccCheckComputeInstanceHasStatus(&instance, "RUNNING"), + ), + }, + }, + }) +} + func testAccCheckComputeInstanceUpdateMachineType(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1783,6 +2305,17 @@ func testAccCheckComputeInstanceHasMinCpuPlatform(instance *compute.Instance, mi } } +func testAccCheckComputeInstanceHasMachineType(instance *compute.Instance, machineType string) resource.TestCheckFunc { + return func(s *terraform.State) error { + instanceMachineType := GetResourceNameFromSelfLink(instance.MachineType) + if instanceMachineType != machineType { + return fmt.Errorf("Wrong machine type: expected %s, got %s", machineType, instanceMachineType) + } + + return nil + } +} + func testAccCheckComputeInstanceHasAliasIpRange(instance *compute.Instance, subnetworkRangeName, iPCidrRange string) resource.TestCheckFunc { return func(s *terraform.State) error { for _, networkInterface := range instance.NetworkInterfaces { @@ -1848,6 +2381,15 @@ func testAccCheckComputeInstanceLacksShieldedVmConfig(instance *computeBeta.Inst } } +func testAccCheckComputeInstanceHasStatus(instance *compute.Instance, status string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instance.Status != status { + return fmt.Errorf("Instance has not status %s, status: %s", status, instance.Status) + } + return nil + } +} + func testAccComputeInstance_basic(instance string) string { return fmt.Sprintf(` data "google_compute_image" "my_image" { @@ -3881,3 +4423,86 @@ resource "google_compute_instance" "foobar" { } `, instance) } + +func testAccComputeInstance_machineType_desiredStatus_allowStoppingForUpdate( + instance, machineType, desiredStatus string, + allowStoppingForUpdate bool, +) string { + desiredStatusConfigSection := "" + if desiredStatus != "" { + desiredStatusConfigSection = fmt.Sprintf( + "desired_status = \"%s\"", + desiredStatus, + ) + } + + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "%s" + zone = "us-central1-a" + can_ip_forward = false + tags = ["foo", "bar"] + + boot_disk { + initialize_params{ + image = "${data.google_compute_image.my_image.self_link}" + } + } + + network_interface { + network = "default" + } + + %s + + metadata = { + foo = "bar" + } + + allow_stopping_for_update = %t +} +`, instance, machineType, desiredStatusConfigSection, allowStoppingForUpdate) +} + +func testAccComputeInstance_desiredStatusTerminatedUpdate(instance string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + zone = "us-central1-a" + can_ip_forward = false + tags = ["baz"] + + boot_disk { + initialize_params{ + image = "${data.google_compute_image.my_image.self_link}" + } + } + + network_interface { + network = "default" + } + + desired_status = "TERMINATED" + + metadata = { + bar = "baz" + } + + labels = { + only_me = "nothing_else" + } +} +`, instance) +} diff --git a/website/docs/r/compute_instance.html.markdown b/website/docs/r/compute_instance.html.markdown index fe363db7f55..85550ab8245 100644 --- a/website/docs/r/compute_instance.html.markdown +++ b/website/docs/r/compute_instance.html.markdown @@ -140,6 +140,9 @@ The following arguments are supported: Structure is documented below. **Note**: [`allow_stopping_for_update`](#allow_stopping_for_update) must be set to true in order to update this field. +* `desired_status` - (Optional) Desired status of the instance. + Either "RUNNING" or "TERMINATED". + * `tags` - (Optional) A list of tags to attach to the instance. * `shielded_instance_config` - (Optional) Enable [Shielded VM](https://cloud.google.com/security/shielded-cloud/shielded-vm) on this instance. Shielded VM provides verifiable integrity to prevent against malware and rootkits. Defaults to disabled. Structure is documented below.