From 7da529f3c1095cdc5b3f08183dd8a7c4cc5756ae Mon Sep 17 00:00:00 2001 From: oceyral Date: Wed, 25 Jul 2018 20:21:20 +0000 Subject: [PATCH] Implement multiple versions for regional instance groups (#1809) --- ...resource_compute_instance_group_manager.go | 12 +- ...e_compute_region_instance_group_manager.go | 142 +++++++++++++++--- ...pute_region_instance_group_manager_test.go | 133 ++++++++++++++++ ...egion_instance_group_manager.html.markdown | 75 ++++++++- 4 files changed, 330 insertions(+), 32 deletions(-) diff --git a/google/resource_compute_instance_group_manager.go b/google/resource_compute_instance_group_manager.go index c06840ed..28d8683a 100644 --- a/google/resource_compute_instance_group_manager.go +++ b/google/resource_compute_instance_group_manager.go @@ -456,10 +456,10 @@ func resourceComputeInstanceGroupManagerRead(d *schema.ResourceData, meta interf return nil } -// Updates an instance group manager by applying an update strategy (REPLACE, RESTART) respecting a rolling update policy (availability settings, -// interval between updates, and particularly, the type of update PROACTIVE or OPPORTUNISTIC because updates performed by API are considered -// OPPORTUNISTIC by default) -func performUpdate(config *Config, id string, updateStrategy string, rollingUpdatePolicy *computeBeta.InstanceGroupManagerUpdatePolicy, versions []*computeBeta.InstanceGroupManagerVersion, project string, zone string) error { +// Updates an instance group manager by applying the update strategy (REPLACE, RESTART) +// and rolling update policy (PROACTIVE, OPPORTUNISTIC). Updates performed by API +// are OPPORTUNISTIC by default. +func performZoneUpdate(config *Config, id string, updateStrategy string, rollingUpdatePolicy *computeBeta.InstanceGroupManagerUpdatePolicy, versions []*computeBeta.InstanceGroupManagerVersion, project string, zone string) error { if updateStrategy == "RESTART" { managedInstances, err := config.clientComputeBeta.InstanceGroupManagers.ListManagedInstances(project, zone, id).Do() if err != nil { @@ -646,7 +646,7 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte updateStrategy := d.Get("update_strategy").(string) rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) - err = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, nil, project, zone) + err = performZoneUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, nil, project, zone) d.SetPartial("instance_template") } @@ -655,7 +655,7 @@ func resourceComputeInstanceGroupManagerUpdate(d *schema.ResourceData, meta inte updateStrategy := d.Get("update_strategy").(string) rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) versions := expandVersions(d.Get("version").([]interface{})) - err = performUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, versions, project, zone) + err = performZoneUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, versions, project, zone) if err != nil { return err } diff --git a/google/resource_compute_region_instance_group_manager.go b/google/resource_compute_region_instance_group_manager.go index 3656e39b..d38568ec 100644 --- a/google/resource_compute_region_instance_group_manager.go +++ b/google/resource_compute_region_instance_group_manager.go @@ -38,10 +38,50 @@ func resourceComputeRegionInstanceGroupManager() *schema.Resource { "instance_template": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, DiffSuppressFunc: compareSelfLinkRelativePaths, }, + "version": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "instance_template": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: compareSelfLinkRelativePaths, + }, + + "target_size": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fixed": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "percent": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100), + }, + }, + }, + }, + }, + }, + }, + "name": &schema.Schema{ Type: schema.TypeString, Required: true, @@ -242,6 +282,7 @@ func resourceComputeRegionInstanceGroupManagerCreate(d *schema.ResourceData, met NamedPorts: getNamedPortsBeta(d.Get("named_port").([]interface{})), TargetPools: convertStringSet(d.Get("target_pools").(*schema.Set)), AutoHealingPolicies: expandAutoHealingPolicies(d.Get("auto_healing_policies").([]interface{})), + Versions: expandVersions(d.Get("version").([]interface{})), DistributionPolicy: expandDistributionPolicy(d.Get("distribution_policy_zones").(*schema.Set)), // Force send TargetSize to allow size of 0. ForceSendFields: []string{"TargetSize"}, @@ -315,6 +356,9 @@ func resourceComputeRegionInstanceGroupManagerRead(d *schema.ResourceData, meta d.Set("base_instance_name", manager.BaseInstanceName) d.Set("instance_template", manager.InstanceTemplate) + if err := d.Set("version", flattenVersions(manager.Versions)); err != nil { + return err + } d.Set("name", manager.Name) d.Set("region", GetResourceNameFromSelfLink(manager.Region)) d.Set("description", manager.Description) @@ -329,6 +373,11 @@ func resourceComputeRegionInstanceGroupManagerRead(d *schema.ResourceData, meta return err } d.Set("self_link", ConvertSelfLinkToV1(manager.SelfLink)) + update_strategy, ok := d.GetOk("update_strategy") + if !ok { + update_strategy = "NONE" + } + d.Set("update_strategy", update_strategy.(string)) if d.Get("wait_for_instances").(bool) { conf := resource.StateChangeConf{ @@ -346,6 +395,63 @@ func resourceComputeRegionInstanceGroupManagerRead(d *schema.ResourceData, meta return nil } +// Updates an instance group manager by applying the update strategy (REPLACE, RESTART) +// and rolling update policy (PROACTIVE, OPPORTUNISTIC). Updates performed by API +// are OPPORTUNISTIC by default. +func performRegionUpdate(config *Config, id string, updateStrategy string, rollingUpdatePolicy *computeBeta.InstanceGroupManagerUpdatePolicy, versions []*computeBeta.InstanceGroupManagerVersion, project string, region string) error { + if updateStrategy == "RESTART" { + managedInstances, err := config.clientComputeBeta.RegionInstanceGroupManagers.ListManagedInstances(project, region, id).Do() + if err != nil { + return fmt.Errorf("Error getting region instance group managers instances: %s", err) + } + + managedInstanceCount := len(managedInstances.ManagedInstances) + instances := make([]string, managedInstanceCount) + for i, v := range managedInstances.ManagedInstances { + instances[i] = v.Instance + } + + recreateInstances := &computeBeta.RegionInstanceGroupManagersRecreateRequest{ + Instances: instances, + } + + op, err := config.clientComputeBeta.RegionInstanceGroupManagers.RecreateInstances(project, region, id, recreateInstances).Do() + if err != nil { + return fmt.Errorf("Error restarting region instance group managers instances: %s", err) + } + + // Wait for the operation to complete + err = computeSharedOperationWaitTime(config.clientCompute, op, project, managedInstanceCount*4, "Restarting RegionInstanceGroupManagers instances") + if err != nil { + return err + } + } + + if updateStrategy == "ROLLING_UPDATE" { + // UpdatePolicy is set for InstanceGroupManager on update only, because it is only relevant for `Patch` calls. + // Other tools(gcloud and UI) capable of executing the same `ROLLING UPDATE` call + // expect those values to be provided by user as part of the call + // or provide their own defaults without respecting what was previously set on UpdateManager. + // To follow the same logic, we provide policy values on relevant update change only. + manager := &computeBeta.InstanceGroupManager{ + UpdatePolicy: rollingUpdatePolicy, + Versions: versions, + } + + op, err := config.clientComputeBeta.RegionInstanceGroupManagers.Patch(project, region, id, manager).Do() + if err != nil { + return fmt.Errorf("Error updating region managed group instances: %s", err) + } + + err = computeSharedOperationWait(config.clientCompute, op, project, "Updating region managed group instances") + if err != nil { + return err + } + } + + return nil +} + func resourceComputeRegionInstanceGroupManagerUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) @@ -406,29 +512,23 @@ func resourceComputeRegionInstanceGroupManagerUpdate(d *schema.ResourceData, met return err } - if d.Get("update_strategy").(string) == "ROLLING_UPDATE" { - // UpdatePolicy is set for InstanceGroupManager on update only, because it is only relevant for `Patch` calls. - // Other tools(gcloud and UI) capable of executing the same `ROLLING UPDATE` call - // expect those values to be provided by user as part of the call - // or provide their own defaults without respecting what was previously set on UpdateManager. - // To follow the same logic, we provide policy values on relevant update change only. - manager := &computeBeta.InstanceGroupManager{ - UpdatePolicy: expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})), - } + updateStrategy := d.Get("update_strategy").(string) + rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) + err = performRegionUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, nil, project, region) + d.SetPartial("instance_template") + } - op, err = config.clientComputeBeta.RegionInstanceGroupManagers.Patch( - project, region, d.Id(), manager).Do() - if err != nil { - return fmt.Errorf("Error updating managed group instances: %s", err) - } - - err = computeSharedOperationWait(config.clientCompute, op, project, "Updating managed group instances") - if err != nil { - return err - } + // If version changes then update + if d.HasChange("version") { + updateStrategy := d.Get("update_strategy").(string) + rollingUpdatePolicy := expandUpdatePolicy(d.Get("rolling_update_policy").([]interface{})) + versions := expandVersions(d.Get("version").([]interface{})) + err = performRegionUpdate(config, d.Id(), updateStrategy, rollingUpdatePolicy, versions, project, region) + if err != nil { + return err } - d.SetPartial("instance_template") + d.SetPartial("version") } if d.HasChange("named_port") { diff --git a/google/resource_compute_region_instance_group_manager_test.go b/google/resource_compute_region_instance_group_manager_test.go index 870b96a7..22a1a889 100644 --- a/google/resource_compute_region_instance_group_manager_test.go +++ b/google/resource_compute_region_instance_group_manager_test.go @@ -236,6 +236,7 @@ func TestAccRegionInstanceGroupManager_rollingUpdatePolicy(t *testing.T) { }, }) } + func TestAccRegionInstanceGroupManager_separateRegions(t *testing.T) { t.Parallel() @@ -262,6 +263,36 @@ func TestAccRegionInstanceGroupManager_separateRegions(t *testing.T) { }) } +func TestAccRegionInstanceGroupManager_versions(t *testing.T) { + t.Parallel() + + var manager computeBeta.InstanceGroupManager + + primaryTemplate := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + canaryTemplate := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + igm := fmt.Sprintf("igm-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRegionInstanceGroupManagerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRegionInstanceGroupManager_versions(primaryTemplate, canaryTemplate, igm), + Check: resource.ComposeTestCheckFunc( + testAccCheckRegionInstanceGroupManagerBetaExists("google_compute_region_instance_group_manager.igm-basic", &manager), + testAccCheckRegionInstanceGroupManagerVersions("google_compute_region_instance_group_manager.igm-basic", primaryTemplate, canaryTemplate), + ), + }, + resource.TestStep{ + ResourceName: "google_compute_region_instance_group_manager.igm-basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccRegionInstanceGroupManager_autoHealingPolicies(t *testing.T) { t.Parallel() @@ -477,6 +508,42 @@ func testAccCheckRegionInstanceGroupManagerNamedPorts(n string, np map[string]in } } +func testAccCheckRegionInstanceGroupManagerVersions(n string, primaryTemplate string, canaryTemplate string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + manager, err := config.clientComputeBeta.RegionInstanceGroupManagers.Get(config.Project, rs.Primary.Attributes["region"], rs.Primary.ID).Do() + if err != nil { + return err + } + + if len(manager.Versions) != 2 { + return fmt.Errorf("Expected # of versions to be 2, got %d", len(manager.Versions)) + } + + primaryVersion := manager.Versions[0] + if !strings.Contains(primaryVersion.InstanceTemplate, primaryTemplate) { + return fmt.Errorf("Expected string \"%s\" to appear in \"%s\"", primaryTemplate, primaryVersion.InstanceTemplate) + } + + canaryVersion := manager.Versions[1] + if !strings.Contains(canaryVersion.InstanceTemplate, canaryTemplate) { + return fmt.Errorf("Expected string \"%s\" to appear in \"%s\"", canaryTemplate, canaryVersion.InstanceTemplate) + } + + return nil + } +} + func testAccCheckRegionInstanceGroupManagerAutoHealingPolicies(n, hck string, initialDelaySec int64) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -954,6 +1021,72 @@ resource "google_compute_http_health_check" "zero" { } `, template, target, igm, hck) } +func testAccRegionInstanceGroupManager_versions(primaryTemplate string, canaryTemplate string, igm string) string { + return fmt.Sprintf(` +resource "google_compute_instance_template" "igm-primary" { + name = "%s" + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + disk { + source_image = "debian-cloud/debian-8-jessie-v20160803" + auto_delete = true + boot = true + } + network_interface { + network = "default" + } + metadata { + foo = "bar" + } + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_instance_template" "igm-canary" { + name = "%s" + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + disk { + source_image = "debian-cloud/debian-8-jessie-v20160803" + auto_delete = true + boot = true + } + network_interface { + network = "default" + } + metadata { + foo = "bar" + } + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_region_instance_group_manager" "igm-basic" { + description = "Terraform test region instance group manager" + name = "%s" + base_instance_name = "igm-basic" + region = "us-central1" + target_size = 2 + + version { + name = "primary" + instance_template = "${google_compute_instance_template.igm-primary.self_link}" + } + + version { + name = "canary" + instance_template = "${google_compute_instance_template.igm-canary.self_link}" + target_size { + fixed = 1 + } + } +} + `, primaryTemplate, canaryTemplate, igm) +} func testAccRegionInstanceGroupManager_distributionPolicy(template, igm string, zones []string) string { return fmt.Sprintf(` 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 993b74ab..959ea039 100644 --- a/website/docs/r/compute_region_instance_group_manager.html.markdown +++ b/website/docs/r/compute_region_instance_group_manager.html.markdown @@ -15,7 +15,7 @@ and [API](https://cloud.google.com/compute/docs/reference/latest/regionInstanceG ~> **Note:** Use [google_compute_instance_group_manager](/docs/providers/google/r/compute_instance_group_manager.html) to create a single-zone instance group manager. -## Example Usage +## Example Usage with top level instance template ```hcl resource "google_compute_health_check" "autohealing" { @@ -55,6 +55,30 @@ resource "google_compute_region_instance_group_manager" "appserver" { ``` +## Example Usage with multiple Versions +```hcl +resource "google_compute_region_instance_group_manager" "appserver" { + name = "appserver-igm" + + base_instance_name = "app" + update_strategy = "NONE" + region = "us-central1" + + target_size = 5 + + version { + instance_template = "${google_compute_instance_template.appserver.self_link}" + } + + version { + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + fixed = 1 + } + } +} +``` + ## Argument Reference The following arguments are supported: @@ -66,8 +90,15 @@ The following arguments are supported: appending a hyphen and a random four-character string to the base instance name. -* `instance_template` - (Required) The full URL to an instance template from - which all new instances will be created. +* `instance_template` - (Optional) The full URL to an instance template from + which all new instances will be created. Conflicts with `version` (see [documentation](https://cloud.google.com/compute/docs/instance-groups/updating-managed-instance-groups#relationship_between_instancetemplate_properties_for_a_managed_instance_group)) + +* `version` - (Optional) Application versions managed by this instance group. Each + version deals with a specific instance template, allowing canary release scenarios. + Conflicts with `instance_template`. Structure is documented below. Beware that + exactly one version must not specify a target size. It means that versions with + a target size will respect the setting, and the one without target size will + be applied to all remaining Instances (top level target_size - each version target_size). * `name` - (Required) The name of the instance group manager. Must be 1-63 characters long and comply with @@ -89,8 +120,8 @@ The following arguments are supported: * `update_strategy` - (Optional, Default `"NONE"`) If the `instance_template` resource is modified, a value of `"NONE"` will prevent any of the managed - instances from being restarted by Terraform. A value of `"ROLLING_UPDATE"` - is supported as [Beta feature]. A value of `"ROLLING_UPDATE"` requires + instances from being restarted by Terraform. A value of `"ROLLING_UPDATE"` + is supported as [Beta feature]. A value of `"ROLLING_UPDATE"` requires `rolling_update_policy` block to be set * `target_size` - (Optional) The target number of running instances for this managed @@ -157,6 +188,40 @@ The **auto_healing_policies** block supports: * `initial_delay_sec` - (Required) The number of seconds that the managed instance group waits before it applies autohealing policies to new instances or recently recreated instances. Between 0 and 3600. +The **version** block supports: + +```hcl +version { + name = "appserver-canary" + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + fixed = 1 + } +} +``` + +```hcl +version { + name = "appserver-canary" + instance_template = "${google_compute_instance_template.appserver-canary.self_link}" + target_size { + percent = 20 + } +} +``` + +* `name` - (Required) - Version name. + +* `instance_template` - (Required) - The full URL to an instance template from which all new instances of this version will be created. + +* `target_size` - (Optional) - The number of instances calculated as a fixed number or a percentage depending on the settings. Structure is documented below. + +The **target_size** block supports: + +* `fixed` - (Optional), The number of instances which are managed for this version. Conflicts with `percent`. + +* `percent` - (Optional), The number of instances (calculated as percentage) which are managed for this version. Conflicts with `fixed`. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are