From 4af7c6ed9083cff4fc832f5ac0287deb56797c0f Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Wed, 26 May 2021 16:39:40 +0000 Subject: [PATCH] Instance group wait for status (#4783) * Adding IGM status * Add markdown, test * Fix return values * PR feedback, documentation * Fix spacing Signed-off-by: Modular Magician --- .changelog/4783.txt | 6 + ...resource_compute_instance_group_manager.go | 121 ++++++++++++- ...rce_compute_instance_group_manager_test.go | 159 ++++++++++++++++++ ...e_compute_region_instance_group_manager.go | 95 ++++++++++- ...mpute_instance_group_manager.html.markdown | 28 +++ ...egion_instance_group_manager.html.markdown | 26 +++ 6 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 .changelog/4783.txt diff --git a/.changelog/4783.txt b/.changelog/4783.txt new file mode 100644 index 0000000000..689edacd4c --- /dev/null +++ b/.changelog/4783.txt @@ -0,0 +1,6 @@ +```release-note:enhancement +compute: added support for output-only `status` field on `google_compute_instance_group_manager` and `google_compute_region_instance_group_manager` +``` +```release-note:enhancement +compute: added support for `wait_for_instances_status` on `google_compute_instance_group_manager` and `google_compute_region_instance_group_manager` +``` diff --git a/google-beta/resource_compute_instance_group_manager.go b/google-beta/resource_compute_instance_group_manager.go index 47d0f148b6..43bef39e9e 100644 --- a/google-beta/resource_compute_instance_group_manager.go +++ b/google-beta/resource_compute_instance_group_manager.go @@ -269,6 +269,13 @@ func resourceComputeInstanceGroupManager() *schema.Resource { Default: false, Description: `Whether to wait for all instances to be created/updated before returning. Note that if this is set to true and the operation does not succeed, Terraform will continue trying until it times out.`, }, + "wait_for_instances_status": { + Type: schema.TypeString, + Optional: true, + Default: "STABLE", + ValidateFunc: validation.StringInSlice([]string{"STABLE", "UPDATED"}, false), + Description: `When used with wait_for_instances specifies the status to wait for. When STABLE is specified this resource will wait until the instances are stable before returning. When UPDATED is set, it will wait for the version target to be reached and any per instance configs to be effective as well as all instances to be stable before returning.`, + }, "stateful_disk": { Type: schema.TypeSet, Optional: true, @@ -295,6 +302,63 @@ func resourceComputeInstanceGroupManager() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "status": { + Type: schema.TypeList, + Computed: true, + Description: `The status of this managed instance group.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_stable": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether the managed instance group is in a stable state. A stable state means that: none of the instances in the managed instance group is currently undergoing any type of change (for example, creation, restart, or deletion); no future changes are scheduled for instances in the managed instance group; and the managed instance group itself is not being modified.`, + }, + + "version_target": { + Type: schema.TypeList, + Computed: true, + Description: `A status of consistency of Instances' versions with their target version specified by version field on Instance Group Manager.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_reached": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether version target has been reached in this managed instance group, i.e. all instances are in their target version. Instances' target version are specified by version field on Instance Group Manager.`, + }, + }, + }, + }, + "stateful": { + Type: schema.TypeList, + Computed: true, + Description: `Stateful status of the given Instance Group Manager.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "has_stateful_config": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether the managed instance group has stateful configuration, that is, if you have configured any items in a stateful policy or in per-instance configs. The group might report that it has no stateful config even when there is still some preserved state on a managed instance, for example, if you have deleted all PICs but not yet applied those deletions.`, + }, + "per_instance_configs": { + Type: schema.TypeList, + Computed: true, + Description: `Status of per-instance configs on the instance.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "all_effective": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating if all of the group's per-instance configs (listed in the output of a listPerInstanceConfigs API call) have status EFFECTIVE or there are no per-instance-configs.`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, UseJSONNumber: true, } @@ -554,12 +618,23 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf if err = d.Set("update_policy", flattenUpdatePolicy(manager.UpdatePolicy)); err != nil { return fmt.Errorf("Error setting update_policy in state: %s", err.Error()) } + if err = d.Set("status", flattenStatus(manager.Status)); err != nil { + return fmt.Errorf("Error setting status in state: %s", err.Error()) + } + + // If unset in state set to default value + if d.Get("wait_for_instances_status").(string) == "" { + if err = d.Set("wait_for_instances_status", "STABLE"); err != nil { + return fmt.Errorf("Error setting wait_for_instances_status in state: %s", err.Error()) + } + } if d.Get("wait_for_instances").(bool) { + waitForUpdates := d.Get("wait_for_instances_status").(string) == "UPDATED" conf := resource.StateChangeConf{ - Pending: []string{"creating", "error"}, + Pending: []string{"creating", "error", "updating per instance configs", "reaching version target"}, Target: []string{"created"}, - Refresh: waitForInstancesRefreshFunc(getManager, d, meta), + Refresh: waitForInstancesRefreshFunc(getManager, waitForUpdates, d, meta), Timeout: d.Timeout(schema.TimeoutCreate), } _, err := conf.WaitForState() @@ -905,10 +980,52 @@ func flattenUpdatePolicy(updatePolicy *computeBeta.InstanceGroupManagerUpdatePol return results } +func flattenStatus(status *computeBeta.InstanceGroupManagerStatus) []map[string]interface{} { + results := []map[string]interface{}{} + data := map[string]interface{}{ + "is_stable": status.IsStable, + "stateful": flattenStatusStateful(status.Stateful), + "version_target": flattenStatusVersionTarget(status.VersionTarget), + } + results = append(results, data) + return results +} + +func flattenStatusStateful(stateful *computeBeta.InstanceGroupManagerStatusStateful) []map[string]interface{} { + results := []map[string]interface{}{} + data := map[string]interface{}{ + "has_stateful_config": stateful.HasStatefulConfig, + "per_instance_configs": flattenStatusStatefulConfigs(stateful.PerInstanceConfigs), + } + results = append(results, data) + return results +} + +func flattenStatusStatefulConfigs(statefulConfigs *computeBeta.InstanceGroupManagerStatusStatefulPerInstanceConfigs) []map[string]interface{} { + results := []map[string]interface{}{} + data := map[string]interface{}{ + "all_effective": statefulConfigs.AllEffective, + } + results = append(results, data) + return results +} + +func flattenStatusVersionTarget(versionTarget *computeBeta.InstanceGroupManagerStatusVersionTarget) []map[string]interface{} { + results := []map[string]interface{}{} + data := map[string]interface{}{ + "is_reached": versionTarget.IsReached, + } + results = append(results, data) + return results +} + func resourceInstanceGroupManagerStateImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { if err := d.Set("wait_for_instances", false); err != nil { return nil, fmt.Errorf("Error setting wait_for_instances: %s", err) } + if err := d.Set("wait_for_instances_status", "STABLE"); err != nil { + return nil, fmt.Errorf("Error setting wait_for_instances_status: %s", err) + } config := meta.(*Config) if err := parseImportId([]string{"projects/(?P[^/]+)/zones/(?P[^/]+)/instanceGroupManagers/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config); err != nil { return nil, err diff --git a/google-beta/resource_compute_instance_group_manager_test.go b/google-beta/resource_compute_instance_group_manager_test.go index cba4a66665..baca4bfb1f 100644 --- a/google-beta/resource_compute_instance_group_manager_test.go +++ b/google-beta/resource_compute_instance_group_manager_test.go @@ -373,6 +373,41 @@ func TestAccInstanceGroupManager_stateful(t *testing.T) { }) } +func TestAccInstanceGroupManager_waitForStatus(t *testing.T) { + t.Parallel() + + template := fmt.Sprintf("tf-test-igm-%s", randString(t, 10)) + target := fmt.Sprintf("tf-test-igm-%s", randString(t, 10)) + igm := fmt.Sprintf("tf-test-igm-%s", randString(t, 10)) + perInstanceConfig := fmt.Sprintf("tf-test-config-%s", randString(t, 10)) + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceGroupManagerDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccInstanceGroupManager_waitForStatus(template, target, igm, perInstanceConfig), + }, + { + ResourceName: "google_compute_instance_group_manager.igm-basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"status", "wait_for_instances_status", "wait_for_instances"}, + }, + { + Config: testAccInstanceGroupManager_waitForStatusUpdated(template, target, igm, perInstanceConfig), + }, + { + ResourceName: "google_compute_instance_group_manager.igm-basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"status", "wait_for_instances_status", "wait_for_instances"}, + }, + }, + }) +} + func testAccCheckInstanceGroupManagerDestroyProducer(t *testing.T) func(s *terraform.State) error { return func(s *terraform.State) error { config := googleProviderConfig(t) @@ -1423,3 +1458,127 @@ resource "google_compute_http_health_check" "zero" { } `, template, target, igm, hck) } + +func testAccInstanceGroupManager_waitForStatus(template, target, igm, perInstanceConfig string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance_template" "igm-basic" { + name = "%s" + machine_type = "e2-medium" + can_ip_forward = false + tags = ["foo", "bar"] + disk { + source_image = data.google_compute_image.my_image.self_link + auto_delete = true + boot = true + device_name = "my-stateful-disk" + } + + network_interface { + network = "default" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_target_pool" "igm-basic" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + session_affinity = "CLIENT_IP_PROTO" +} + +resource "google_compute_instance_group_manager" "igm-basic" { + description = "Terraform test instance group manager" + name = "%s" + version { + instance_template = google_compute_instance_template.igm-basic.self_link + name = "prod" + } + target_pools = [google_compute_target_pool.igm-basic.self_link] + base_instance_name = "igm-basic" + zone = "us-central1-c" + wait_for_instances = true + wait_for_instances_status = "STABLE" +} + +resource "google_compute_per_instance_config" "per-instance" { + instance_group_manager = google_compute_instance_group_manager.igm-basic.name + zone = "us-central1-c" + name = "%s" + remove_instance_state_on_destroy = true + preserved_state { + metadata = { + foo = "bar" + } + } +} +`, template, target, igm, perInstanceConfig) +} + +func testAccInstanceGroupManager_waitForStatusUpdated(template, target, igm, perInstanceConfig string) string { + return fmt.Sprintf(` +data "google_compute_image" "my_image" { + family = "debian-9" + project = "debian-cloud" +} + +resource "google_compute_instance_template" "igm-basic" { + name = "%s" + machine_type = "e2-medium" + can_ip_forward = false + tags = ["foo", "bar"] + disk { + source_image = data.google_compute_image.my_image.self_link + auto_delete = true + boot = true + device_name = "my-stateful-disk" + } + + network_interface { + network = "default" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_target_pool" "igm-basic" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + session_affinity = "CLIENT_IP_PROTO" +} + +resource "google_compute_instance_group_manager" "igm-basic" { + description = "Terraform test instance group manager" + name = "%s" + version { + instance_template = google_compute_instance_template.igm-basic.self_link + name = "prod" + } + target_pools = [google_compute_target_pool.igm-basic.self_link] + base_instance_name = "igm-basic" + zone = "us-central1-c" + wait_for_instances = true + wait_for_instances_status = "UPDATED" +} + +resource "google_compute_per_instance_config" "per-instance" { + instance_group_manager = google_compute_instance_group_manager.igm-basic.name + zone = "us-central1-c" + name = "%s" + remove_instance_state_on_destroy = true + preserved_state { + metadata = { + foo = "baz" + } + } +} +`, template, target, igm, perInstanceConfig) +} diff --git a/google-beta/resource_compute_region_instance_group_manager.go b/google-beta/resource_compute_region_instance_group_manager.go index f8daab1452..aecbfe8023 100644 --- a/google-beta/resource_compute_region_instance_group_manager.go +++ b/google-beta/resource_compute_region_instance_group_manager.go @@ -175,6 +175,13 @@ func resourceComputeRegionInstanceGroupManager() *schema.Resource { Default: false, Description: `Whether to wait for all instances to be created/updated before returning. Note that if this is set to true and the operation does not succeed, Terraform will continue trying until it times out.`, }, + "wait_for_instances_status": { + Type: schema.TypeString, + Optional: true, + Default: "STABLE", + ValidateFunc: validation.StringInSlice([]string{"STABLE", "UPDATED"}, false), + Description: `When used with wait_for_instances specifies the status to wait for. When STABLE is specified this resource will wait until the instances are stable before returning. When UPDATED is set, it will wait for the version target to be reached and any per instance configs to be effective as well as all instances to be stable before returning.`, + }, "auto_healing_policies": { Type: schema.TypeList, @@ -321,6 +328,63 @@ func resourceComputeRegionInstanceGroupManager() *schema.Resource { }, }, }, + "status": { + Type: schema.TypeList, + Computed: true, + Description: `The status of this managed instance group.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_stable": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether the managed instance group is in a stable state. A stable state means that: none of the instances in the managed instance group is currently undergoing any type of change (for example, creation, restart, or deletion); no future changes are scheduled for instances in the managed instance group; and the managed instance group itself is not being modified.`, + }, + + "version_target": { + Type: schema.TypeList, + Computed: true, + Description: `A status of consistency of Instances' versions with their target version specified by version field on Instance Group Manager.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "is_reached": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether version target has been reached in this managed instance group, i.e. all instances are in their target version. Instances' target version are specified by version field on Instance Group Manager.`, + }, + }, + }, + }, + "stateful": { + Type: schema.TypeList, + Computed: true, + Description: `Stateful status of the given Instance Group Manager.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "has_stateful_config": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating whether the managed instance group has stateful configuration, that is, if you have configured any items in a stateful policy or in per-instance configs. The group might report that it has no stateful config even when there is still some preserved state on a managed instance, for example, if you have deleted all PICs but not yet applied those deletions.`, + }, + "per_instance_configs": { + Type: schema.TypeList, + Computed: true, + Description: `Status of per-instance configs on the instance.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "all_effective": { + Type: schema.TypeBool, + Computed: true, + Description: `A bit indicating if all of the group's per-instance configs (listed in the output of a listPerInstanceConfigs API call) have status EFFECTIVE or there are no per-instance-configs.`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, }, UseJSONNumber: true, } @@ -408,7 +472,7 @@ func getRegionalManager(d *schema.ResourceData, meta interface{}) (*computeBeta. return manager, nil } -func waitForInstancesRefreshFunc(f getInstanceManagerFunc, d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { +func waitForInstancesRefreshFunc(f getInstanceManagerFunc, waitForUpdates bool, d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc { return func() (interface{}, string, error) { m, err := f(d, meta) if err != nil { @@ -416,6 +480,17 @@ func waitForInstancesRefreshFunc(f getInstanceManagerFunc, d *schema.ResourceDat return nil, "error", err } if m.Status.IsStable { + if waitForUpdates { + // waitForUpdates waits for versions to be reached and per instance configs to be updated (if present) + if m.Status.Stateful.HasStatefulConfig { + if !m.Status.Stateful.PerInstanceConfigs.AllEffective { + return false, "updating per instance configs", nil + } + } + if !m.Status.VersionTarget.IsReached { + return false, "reaching version target", nil + } + } return true, "created", nil } else { return false, "creating", nil @@ -493,12 +568,23 @@ func resourceComputeRegionInstanceGroupManagerRead(d *schema.ResourceData, meta if err = d.Set("stateful_disk", flattenStatefulPolicy(manager.StatefulPolicy)); err != nil { return fmt.Errorf("Error setting stateful_disk in state: %s", err.Error()) } + if err = d.Set("status", flattenStatus(manager.Status)); err != nil { + return fmt.Errorf("Error setting status in state: %s", err.Error()) + } + + // If unset in state set to default value + if d.Get("wait_for_instances_status").(string) == "" { + if err = d.Set("wait_for_instances_status", "STABLE"); err != nil { + return fmt.Errorf("Error setting wait_for_instances_status in state: %s", err.Error()) + } + } if d.Get("wait_for_instances").(bool) { + waitForUpdates := d.Get("wait_for_instances_status").(string) == "UPDATED" conf := resource.StateChangeConf{ - Pending: []string{"creating", "error"}, + Pending: []string{"creating", "error", "updating per instance configs", "reaching version target"}, Target: []string{"created"}, - Refresh: waitForInstancesRefreshFunc(getRegionalManager, d, meta), + Refresh: waitForInstancesRefreshFunc(getRegionalManager, waitForUpdates, d, meta), Timeout: d.Timeout(schema.TimeoutCreate), } _, err := conf.WaitForState() @@ -770,6 +856,9 @@ func resourceRegionInstanceGroupManagerStateImporter(d *schema.ResourceData, met if err := d.Set("wait_for_instances", false); err != nil { return nil, fmt.Errorf("Error setting wait_for_instances: %s", err) } + if err := d.Set("wait_for_instances_status", "STABLE"); err != nil { + return nil, fmt.Errorf("Error setting wait_for_instances_status: %s", err) + } config := meta.(*Config) if err := parseImportId([]string{"projects/(?P[^/]+)/regions/(?P[^/]+)/instanceGroupManagers/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)/(?P[^/]+)", "(?P[^/]+)"}, d, config); err != nil { return nil, err diff --git a/website/docs/r/compute_instance_group_manager.html.markdown b/website/docs/r/compute_instance_group_manager.html.markdown index cebe99fdfd..40feed2527 100644 --- a/website/docs/r/compute_instance_group_manager.html.markdown +++ b/website/docs/r/compute_instance_group_manager.html.markdown @@ -129,6 +129,11 @@ The following arguments are supported: returning. Note that if this is set to true and the operation does not succeed, Terraform will continue trying until it times out. +* `wait_for_instances_status` - (Optional) When used with `wait_for_instances` it specifies the status to wait for. + When `STABLE` is specified this resource will wait until the instances are stable before returning. When `UPDATED` is + set, it will wait for the version target to be reached and any per instance configs to be effective as well as all + instances to be stable before returning. The possible values are `STABLE` and `UPDATED` + --- * `auto_healing_policies` - (Optional) The autohealing policies for this managed instance @@ -246,6 +251,29 @@ exported: * `self_link` - The URL of the created resource. +* `status` - The status of this managed instance group. + +The `status` block holds: + +* `is_stable` - A bit indicating whether the managed instance group is in a stable state. A stable state means that: none of the instances in the managed instance group is currently undergoing any type of change (for example, creation, restart, or deletion); no future changes are scheduled for instances in the managed instance group; and the managed instance group itself is not being modified. + +* `version_target` - A status of consistency of Instances' versions with their target version specified by version field on Instance Group Manager. + +The `version_target` block holds: + +* `version_target` - A bit indicating whether version target has been reached in this managed instance group, i.e. all instances are in their target version. Instances' target version are specified by version field on Instance Group Manager. + +* `stateful` - Stateful status of the given Instance Group Manager. + +The `stateful` block holds: + +* `has_stateful_config` - A bit indicating whether the managed instance group has stateful configuration, that is, if you have configured any items in a stateful policy or in per-instance configs. The group might report that it has no stateful config even when there is still some preserved state on a managed instance, for example, if you have deleted all PICs but not yet applied those deletions. + +* `per_instance_configs` - Status of per-instance configs on the instance. + +The `per_instance_configs` block holds: + +* `all_effective` - A bit indicating if all of the group's per-instance configs (listed in the output of a listPerInstanceConfigs API call) have status `EFFECTIVE` or there are no per-instance-configs. ## Timeouts diff --git a/website/docs/r/compute_region_instance_group_manager.html.markdown b/website/docs/r/compute_region_instance_group_manager.html.markdown index c9ae0d847a..011bc7b83d 100644 --- a/website/docs/r/compute_region_instance_group_manager.html.markdown +++ b/website/docs/r/compute_region_instance_group_manager.html.markdown @@ -131,6 +131,11 @@ The following arguments are supported: returning. Note that if this is set to true and the operation does not succeed, Terraform will continue trying until it times out. +* `wait_for_instances_status` - (Optional) When used with `wait_for_instances` it specifies the status to wait for. + When `STABLE` is specified this resource will wait until the instances are stable before returning. When `UPDATED` is + set, it will wait for the version target to be reached and any per instance configs to be effective as well as all + instances to be stable before returning. The possible values are `STABLE` and `UPDATED` + --- * `auto_healing_policies` - (Optional) The autohealing policies for this managed instance @@ -256,6 +261,27 @@ exported: * `self_link` - The URL of the created resource. +The `status` block holds: + +* `is_stable` - A bit indicating whether the managed instance group is in a stable state. A stable state means that: none of the instances in the managed instance group is currently undergoing any type of change (for example, creation, restart, or deletion); no future changes are scheduled for instances in the managed instance group; and the managed instance group itself is not being modified. + +* `version_target` - A status of consistency of Instances' versions with their target version specified by version field on Instance Group Manager. + +The `version_target` block holds: + +* `version_target` - A bit indicating whether version target has been reached in this managed instance group, i.e. all instances are in their target version. Instances' target version are specified by version field on Instance Group Manager. + +* `stateful` - Stateful status of the given Instance Group Manager. + +The `stateful` block holds: + +* `has_stateful_config` - A bit indicating whether the managed instance group has stateful configuration, that is, if you have configured any items in a stateful policy or in per-instance configs. The group might report that it has no stateful config even when there is still some preserved state on a managed instance, for example, if you have deleted all PICs but not yet applied those deletions. + +* `per_instance_configs` - Status of per-instance configs on the instance. + +The `per_instance_configs` block holds: + +* `all_effective` - A bit indicating if all of the group's per-instance configs (listed in the output of a listPerInstanceConfigs API call) have status `EFFECTIVE` or there are no per-instance-configs. ## Timeouts